审计修复:后端(安全 / 数据一致性 / 性能)
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:
+36
-17
@@ -86,6 +86,10 @@ type Page struct {
|
||||
// BackQS 是当前页面地址(含查询串、去掉 back 自己)编码后的 ?back= 参数值,
|
||||
// 列表页生成指向详情页的链接时用它。必须整串编码:里面有 & 时不编码会把后面的参数截断。
|
||||
BackQS template.URL
|
||||
|
||||
// SelfPath 是 BackQS 的**未编码**原文。需要在这一层再编码一次的场景(编辑页的
|
||||
// ?back=)必须用它——拿已编码的 BackQS 再编码会变成 %252F…(见 EditPlaceLink)。
|
||||
SelfPath string
|
||||
}
|
||||
|
||||
// IsAdmin 报告当前访问者是否已登录后台。
|
||||
@@ -143,12 +147,12 @@ func (p Page) filterPairs() []string {
|
||||
return kv
|
||||
}
|
||||
|
||||
// backQS 把当前请求的地址编码成 ?back= 的值。
|
||||
// selfPath 是当前请求的地址(路径 + 规范化后的查询串),**未编码**。
|
||||
//
|
||||
// 去掉 back 自身是必须的:详情页的地址里本来就带着 back(来自列表页),
|
||||
// 原样再编码一层会变成套娃,回退一次只退到上一层的自己。
|
||||
// 用 url.Values.Encode() 顺带把参数规范化(排序、重新编码),服务端解析结果不变。
|
||||
func backQS(r *http.Request) template.URL {
|
||||
func selfPath(r *http.Request) string {
|
||||
q := r.URL.Query()
|
||||
q.Del("back")
|
||||
// ok / err 是重定向带回来的一次性提示,挂进 back 会让它在下一页再弹一次
|
||||
@@ -158,27 +162,39 @@ func backQS(r *http.Request) template.URL {
|
||||
if e := q.Encode(); e != "" {
|
||||
u += "?" + e
|
||||
}
|
||||
return template.URL(url.QueryEscape(u))
|
||||
return u
|
||||
}
|
||||
|
||||
// backFromRequest 取出 ?back= 并只放行**站内路径**。
|
||||
// backQS 是 selfPath 的编码形式,直接当 ?back= 的值用。
|
||||
// 需要在这一层再编码一次的场景要用 selfPath,别拿它再转一遍(会变成 %252F…)。
|
||||
func backQS(r *http.Request) template.URL {
|
||||
return template.URL(url.QueryEscape(selfPath(r)))
|
||||
}
|
||||
|
||||
// safeInternalPath 只放行**站内路径**,不合法时返回空串。
|
||||
//
|
||||
// 返回值会当 template.URL 用(绕过 html/template 的 URL 转义),所以这里必须挡死
|
||||
// 返回值会当 template.URL 用(绕过 html/template 的 URL 转义),所以必须挡死
|
||||
// 「//evil.com」这种协议相对地址,以及引号/尖括号这类能跳出属性的字符 ——
|
||||
// 否则只要有人把链接拼成 /places/1?back=https://evil.com,这里就成了开放重定向。
|
||||
func backFromRequest(r *http.Request) template.URL {
|
||||
b := strings.TrimSpace(r.URL.Query().Get("back"))
|
||||
if b == "" || !strings.HasPrefix(b, "/") || strings.HasPrefix(b, "//") {
|
||||
// 控制字符(含 %00、%0b)也一律不收:浏览器会把 NUL 之类的字符悄悄吃掉,
|
||||
// 留着就是给「同一串地址在不同环节解析成不同东西」留口子。
|
||||
//
|
||||
// ?back=、后台表单的 next、投票后的跳转三处共用这一份判据,别再各写一遍。
|
||||
func safeInternalPath(p string) string {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" || !strings.HasPrefix(p, "/") || strings.HasPrefix(p, "//") {
|
||||
return ""
|
||||
}
|
||||
for i := 0; i < len(b); i++ {
|
||||
// 控制字符(含 %00、%0b 这类)与能跳出属性的符号一律不收:
|
||||
// 浏览器会把 NUL 之类的字符悄悄吃掉,留着就是给「同一串地址在不同环节解析成不同东西」留口子
|
||||
if c := b[i]; c < 0x20 || c == 0x7f || c == '"' || c == '\'' || c == '<' || c == '>' || c == '\\' {
|
||||
for i := 0; i < len(p); i++ {
|
||||
if c := p[i]; c < 0x20 || c == 0x7f || c == '"' || c == '\'' || c == '<' || c == '>' || c == '\\' {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return template.URL(b)
|
||||
return p
|
||||
}
|
||||
|
||||
func backFromRequest(r *http.Request) template.URL {
|
||||
return template.URL(safeInternalPath(r.URL.Query().Get("back")))
|
||||
}
|
||||
|
||||
// BackRaw 是 Back 的纯字符串形式,给**非 href** 的场景用(表单的 hidden 字段):
|
||||
@@ -231,14 +247,18 @@ func (p Page) PlaceLink(id int64) template.URL {
|
||||
|
||||
// EditPlaceLink 是「到数据编辑页打开这个地点」,同样带上返回地址。
|
||||
// 优先用列表页给的 Back(站长要的是「改完回地点库」),没有就退回当前这一页。
|
||||
//
|
||||
// 这里必须用**未编码**的 SelfPath:BackQS 已经 QueryEscape 过一次,
|
||||
// 再转一次就成了 %252F…,服务端解出来不以 / 开头会被当成站外地址丢掉,
|
||||
// 于是「直接打开 /places/1 再点编辑」的编辑页上返回按钮凭空消失。
|
||||
func (p Page) EditPlaceLink(srcPK int) template.URL {
|
||||
back := p.Back
|
||||
back := string(p.Back)
|
||||
if back == "" {
|
||||
back = p.BackQS
|
||||
back = p.SelfPath
|
||||
}
|
||||
u := "/admin/edit?open=" + strconv.Itoa(srcPK)
|
||||
if back != "" {
|
||||
u += "&back=" + url.QueryEscape(string(back))
|
||||
u += "&back=" + url.QueryEscape(back)
|
||||
}
|
||||
return template.URL(u)
|
||||
}
|
||||
@@ -1364,7 +1384,6 @@ func templateFuncs() template.FuncMap {
|
||||
}
|
||||
return b
|
||||
},
|
||||
"safe": func(s string) template.HTML { return template.HTML(s) },
|
||||
"colorof": colorOf,
|
||||
"icon": iconOf,
|
||||
"iconor": iconOr,
|
||||
|
||||
Reference in New Issue
Block a user