Files
rond-with-you/internal/web/download_name_test.go
T
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

128 lines
4.2 KiB
Go
Raw 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 (
"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)
}
}