审计修复:后端(安全 / 数据一致性 / 性能)
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user