Files
tamakyi b4c2a8452f 审计修复:后端(安全 / 数据一致性 / 性能)
audit-2026-09-21.md 里的后端条目落地。每条都配了回归守卫,并逐条做过变异验证
(注入原 bug → 确认按正确理由变红 → 还原 → 变绿)。

安全
- CSRF 只加同源 Origin/Referer 守卫(csrf.go),不动模板
- 会话 cookie 的 Secure 按请求判断;两步验证两闸(loginGuard + 验证码一次性)
- 跳转收口到 safeInternalPath;clientIP 只认 XFF 最后一段
- GPX 按属主圈定且查 RowsAffected;下载名走 downloadDisposition
- 上传口统一 MaxBytesReader;落盘后的失败路径清掉半成品

数据一致性
- 区域判定两端各过一遍再 AND(apiTrack 与 movementsWhere 对齐)
- Filter.Verdict 全域一致(下推到 visitsWhere);聚合刷新必须返回 error
- 附属数据改挂坐标键,Go 与 SQL 两套表达式逐位一致
- 脏备份的伪时间/越界坐标不再进领域模型;rows 循环统一 drainRows
- 合并与导出:writeRawRow 上抛错误、悬空引用清理、原生列补全

性能
- placeCols 相关子查询带 dataset_id 前导列;findSourceArchive 先按大小筛
- 批量勾选改 ANY($3::bigint[]);fillupCache 加上限;同一查询不再查两遍

另:新增「全新库」冒烟测试 —— 空库跑完 Migrate 再用应用侧读查询真打一遍,
迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
2026-09-24 20:37:16 +08:00

71 lines
2.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package web
import (
"log"
"net/http"
"net/url"
"strings"
)
// csrfOriginGuard 挡「跨站发起的写请求」。
//
// 会话 cookie 是 SameSite=Lax,跨站 POST 本来就带不上 cookie;这层是纵深防御,
// 顺手把「将来有人把 SameSite 放宽」和「同站不同源」这两条路一起堵掉。
//
// 判据只认浏览器一定会带的两个头,不需要给每个表单塞 token,因此模板一行都不用改:
// - 有 Origin:必须是本站;`Origin: null`(沙箱 iframe / file://)一律拒。
// - 没有 Origin 但有 Referer:必须是本站。
// - 两个都没有:放行 —— 跨站 POST 浏览器必带 Origin(只有 Referrer-Policy 把它
// 一起抹掉的情况,而那时 Lax 已经把 cookie 拦在外面了),所以这里只可能是
// curl / 脚本这类非浏览器客户端。
//
// 注意:反代必须把**原始 Host** 传过来(nginx 的 `proxy_set_header Host $host`),
// 并且改了 Host 时要带 X-Forwarded-Host;否则本站自己的表单也会被拒。
func (s *Server) csrfOriginGuard(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
default:
next.ServeHTTP(w, r)
return
}
if !sameOriginRequest(r) {
log.Printf("拒绝跨站写请求 %s %s:Origin=%q Referer=%q Host=%q",
r.Method, r.URL.Path, r.Header.Get("Origin"), r.Header.Get("Referer"), r.Host)
http.Error(w, "跨站请求已被拒绝", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
// sameOriginRequest 判断写请求是不是本站自己发起的。
func sameOriginRequest(r *http.Request) bool {
if o := r.Header.Get("Origin"); o != "" {
return matchesRequestHost(r, o)
}
if ref := r.Header.Get("Referer"); ref != "" {
return matchesRequestHost(r, ref)
}
return true
}
// matchesRequestHost 比较 raw(Origin 或 Referer)的 host 与本请求的 host。
// X-Forwarded-Host 也在候选里:反代改写 Host 时靠它兜底。
func matchesRequestHost(r *http.Request, raw string) bool {
u, err := url.Parse(raw)
if err != nil || u.Host == "" {
return false // 含 "null"、相对地址等无法定位来源的形态
}
if strings.EqualFold(u.Host, r.Host) {
return true
}
if xfh := r.Header.Get("X-Forwarded-Host"); xfh != "" {
if i := strings.IndexByte(xfh, ','); i >= 0 {
xfh = xfh[:i]
}
return strings.EqualFold(u.Host, strings.TrimSpace(xfh))
}
return false
}