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 再用应用侧读查询真打一遍, 迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
128 lines
4.2 KiB
Go
128 lines
4.2 KiB
Go
package web
|
||
|
||
import (
|
||
"net/url"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// S9 回归:下载文件名不能从引号里「逃出去」。
|
||
//
|
||
// 导出包名里带着 `datasets.original_name`,也就是上传时的文件名。它原样拼进
|
||
// `filename="…"` 时,一个 `"` 就能提前闭合引号、把后面的内容变成新的头参数
|
||
// (`x"; filename="evil.exe`)。net/http 只把 CR/LF 换成空格,引号它不管。
|
||
func TestDownloadDispositionCannotEscapeTheHeader(t *testing.T) {
|
||
// 结构:attachment; filename="<纯 ASCII 安全串>"; filename*=UTF-8''<百分号编码>
|
||
// 集合里必须带上 `-`:attr-char 允许它,而日期段 `-export-20260921` 全是它。
|
||
shape := regexp.MustCompile(`^attachment; filename="([^"]*)"; filename\*=UTF-8''([-A-Za-z0-9._~%]*)$`)
|
||
|
||
cases := []struct {
|
||
in string
|
||
want string // 期望 filename* 解回来的原名;空表示与 in 相同
|
||
}{
|
||
{`普通名字-export-20260921.rondbackup`, ""},
|
||
{`a"b.rondbackup`, ""},
|
||
{`a"; filename="evil.exe`, ""},
|
||
{"换\n行\r.rondbackup", ""},
|
||
{`..%2f..%2fetc%2fpasswd`, ""},
|
||
{`C:\Windows\system32\x.rondbackup`, ""},
|
||
{`a;b=c:d*e?f<g>h|i.rondbackup`, ""},
|
||
{`日本語のバックアップ.rondbackup`, ""},
|
||
{"", "download"},
|
||
{" ", "download"},
|
||
}
|
||
|
||
for _, c := range cases {
|
||
got := downloadDisposition(c.in)
|
||
|
||
if strings.ContainsAny(got, "\r\n") {
|
||
t.Errorf("%q → 头部里出现了裸的换行: %q", c.in, got)
|
||
}
|
||
if n := strings.Count(got, `"`); n != 2 {
|
||
t.Errorf("%q → 引号有 %d 个(必须恰好一对,多了就能逃逸): %q", c.in, n, got)
|
||
}
|
||
m := shape.FindStringSubmatch(got)
|
||
if m == nil {
|
||
t.Errorf("%q → 头部形状不对: %q", c.in, got)
|
||
continue
|
||
}
|
||
if strings.ContainsAny(m[1], `"\\/:;:=*?<>|`) {
|
||
t.Errorf("%q → filename= 里还有危险字符: %q", c.in, m[1])
|
||
}
|
||
// 非 ASCII 名字才需要 filename* 兜底;这里统一验「能解回原名」
|
||
back, err := url.PathUnescape(m[2])
|
||
if err != nil {
|
||
t.Fatalf("%q → filename*= 不是合法的百分号编码: %v", c.in, err)
|
||
}
|
||
want := c.want
|
||
if want == "" {
|
||
want = c.in
|
||
}
|
||
if back != want {
|
||
t.Errorf("%q → filename*= 解回来是 %q,期望 %q", c.in, back, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 纯中文名会把 filename= 整串换成下划线,此时必须保留扩展名,
|
||
// 否则用户下到手里是个没法双击打开的「download」。
|
||
func TestDownloadDispositionKeepsExtensionOnNonASCII(t *testing.T) {
|
||
got := downloadDisposition("足迹备份.rondbackup")
|
||
if !strings.Contains(got, `.rondbackup"`) {
|
||
t.Errorf("非 ASCII 名的 ASCII 兜底丢了扩展名: %q", got)
|
||
}
|
||
if !strings.HasPrefix(got, `attachment; filename="download`) {
|
||
t.Errorf("非 ASCII 名应当退到可读的兜底名: %q", got)
|
||
}
|
||
}
|
||
|
||
// S9 静态守卫:所有 Content-Disposition 都必须走 downloadDisposition,
|
||
// 不许再手拼 f-string——手拼就是这次的成因,而且它只在特定文件名下出问题。
|
||
func TestAllContentDispositionGoThroughHelper(t *testing.T) {
|
||
dir := pkgFile(t, "")
|
||
entries, err := os.ReadDir(dir)
|
||
if err != nil {
|
||
t.Fatalf("读包目录失败: %v", err)
|
||
}
|
||
scanned, found := 0, 0
|
||
for _, e := range entries {
|
||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".go") || strings.HasSuffix(e.Name(), "_test.go") {
|
||
continue
|
||
}
|
||
scanned++
|
||
data, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
||
if err != nil {
|
||
t.Fatalf("读 %s 失败: %v", e.Name(), err)
|
||
}
|
||
src := string(data)
|
||
for i := 0; ; {
|
||
j := strings.Index(src[i:], `"Content-Disposition"`)
|
||
if j < 0 {
|
||
break
|
||
}
|
||
j += i
|
||
found++
|
||
// 取一小段窗口看这次 Set 是不是用的助手
|
||
win := src[j:]
|
||
if len(win) > 200 {
|
||
win = win[:200]
|
||
}
|
||
if !strings.Contains(win, "downloadDisposition(") {
|
||
line := 1 + strings.Count(src[:j], "\n")
|
||
t.Errorf("%s:%d 的 Content-Disposition 没有走 downloadDisposition:%s",
|
||
e.Name(), line, strings.TrimSpace(strings.SplitN(win, "\n", 2)[0]))
|
||
}
|
||
i = j + 1
|
||
}
|
||
}
|
||
if scanned < 5 {
|
||
t.Fatalf("只扫了 %d 个源文件,守卫形同虚设", scanned)
|
||
}
|
||
if found < 3 {
|
||
t.Fatalf("只找到 %d 处 Content-Disposition(期望至少 3 处),守卫判据需要更新", found)
|
||
}
|
||
}
|