审计修复:后端(安全 / 数据一致性 / 性能)

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 再用应用侧读查询真打一遍,
迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
This commit is contained in:
tamakyi
2026-09-24 20:37:16 +08:00
parent 78865fc736
commit b4c2a8452f
70 changed files with 5417 additions and 360 deletions
+46 -10
View File
@@ -7,6 +7,7 @@ import (
"encoding/hex"
"net"
"net/http"
"net/netip"
"strconv"
"strings"
@@ -17,18 +18,53 @@ import (
// voterCookie 给匿名访客分配稳定身份,用于「一人一票」去重。
const voterCookie = "rond_voter"
// clientIP 取访客 IP,优先用反代透传的 X-Forwarded-For 第一段。
// clientIP 取访客 IP,匿名投票身份用它叠一层「同一来源」的识别。
//
// X-Forwarded-For 客户端自己也能发,所以这里有两个讲究:
// 1. 取**最后一段**而不是第一段。nginx 的 `$proxy_add_x_forwarded_for` 是把客户端发来的值
// 原样接在**前面**、再把真实对端补在后面,所以第一段恰恰是最好伪造的那个;最后一段是
// 反代自己写的,客户端改不动。
// 2. 只有最后一段确实能解析成 IP 才采用,否则整个头作废、退回 RemoteAddr——
// 免得一句垃圾字符串被当成身份。
//
// 遗留 1:若本站直接暴露在公网(前面没有反代),客户端仍能自造这个头。要彻底堵上得在最外层
// 覆盖或剥掉它(`proxy_set_header X-Forwarded-For $remote_addr`)。
//
// 遗留 2:反代前面再叠一层 CDN(而反代没配 `real_ip`)时,最后一段会变成 CDN 自己的 IP,
// 于是所有访客被叠成同一个来源。cookie 仍各不相同,只是这层「同源」识别退化成全员共用,
// 不会把谁认成谁。真有多层代理,判据要改成「按已知跳数从右往左取」,不能写死最后一段。
func clientIP(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
if i := strings.IndexByte(xff, ','); i >= 0 {
return strings.TrimSpace(xff[:i])
}
return strings.TrimSpace(xff)
if ip, ok := lastForwardedIP(r.Header.Values("X-Forwarded-For")); ok {
return ip
}
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
return hostOnly(r.RemoteAddr)
}
// lastForwardedIP 取 X-Forwarded-For 的最后一段并校验成 IP。多行头按逗号拼起来看,
// 与实际代理的拼装方式一致;IPv4-mapped IPv6 归一成 IPv4,同一客户端不该因为写法不同而换身份。
func lastForwardedIP(header []string) (string, bool) {
raw := strings.Join(header, ",")
if i := strings.LastIndexByte(raw, ','); i >= 0 {
raw = raw[i+1:]
}
last := strings.TrimSpace(raw)
if addr, err := netip.ParseAddr(last); err == nil {
return addr.Unmap().String(), true
}
if host, _, err := net.SplitHostPort(last); err == nil {
if addr, err := netip.ParseAddr(host); err == nil {
return addr.Unmap().String(), true
}
}
return "", false
}
// hostOnly 去掉 RemoteAddr 里的端口;没有端口就原样返回。
func hostOnly(remoteAddr string) string {
if host, _, err := net.SplitHostPort(remoteAddr); err == nil {
return host
}
return r.RemoteAddr
return remoteAddr
}
// voterKeyFromRequest 从既有 cookie 推导匿名身份;没有 cookie 返回空串。
@@ -238,8 +274,8 @@ func (s *Server) adminPlaceVote(w http.ResponseWriter, r *http.Request) {
s.serverError(w, r, err)
return
}
next := r.PostFormValue("next")
if !strings.HasPrefix(next, "/") {
next := safeInternalPath(r.PostFormValue("next"))
if next == "" {
next = "/admin"
}
http.Redirect(w, r, next, http.StatusSeeOther)