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 再用应用侧读查询真打一遍, 迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
99 lines
3.0 KiB
Go
99 lines
3.0 KiB
Go
package rond
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestCopyLimitedRejectsOverrun 守住备份包解压的体积上限。
|
|
// zip 头里的 UncompressedSize 是攻击者可控字段,不能只信声明——必须在读的时候掐住。
|
|
func TestCopyLimitedRejectsOverrun(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
if err := copyLimited(&buf, strings.NewReader(strings.Repeat("a", 100)), 99); err == nil {
|
|
t.Error("超过上限应当报错")
|
|
}
|
|
|
|
// 恰好等于上限要放过,内容也不能被截断
|
|
buf.Reset()
|
|
if err := copyLimited(&buf, strings.NewReader(strings.Repeat("a", 100)), 100); err != nil {
|
|
t.Errorf("恰好等于上限不该报错: %v", err)
|
|
}
|
|
if buf.Len() != 100 {
|
|
t.Errorf("正常内容被截断了: %d 字节(应为 100)", buf.Len())
|
|
}
|
|
|
|
buf.Reset()
|
|
if err := copyLimited(&buf, strings.NewReader("短"), 100); err != nil {
|
|
t.Errorf("正常内容不该报错: %v", err)
|
|
}
|
|
if buf.String() != "短" {
|
|
t.Errorf("内容被改动了: %q", buf.String())
|
|
}
|
|
}
|
|
|
|
// TestUnpackArchiveEnforcesLimit 确认上限真的接在 Open 的解压路径上,
|
|
// 而不只是 copyLimited 孤零零地正确。上限抽成参数就是为了能在这里用小值验一遍。
|
|
func TestUnpackArchiveEnforcesLimit(t *testing.T) {
|
|
// 真上限(2 GiB)没法用一个小包走一遍,退一步把它圈在「挡得住 zip bomb、
|
|
// 又不至于误伤真备份」的区间里——顺手拦住把常量改成 1<<62 这类手滑。
|
|
if maxDBBytes < 1<<30 || maxDBBytes > 8<<30 {
|
|
t.Errorf("maxDBBytes=%d 超出合理区间 [1GiB, 8GiB]", maxDBBytes)
|
|
}
|
|
|
|
const payload = 4096
|
|
var buf bytes.Buffer
|
|
zw := zip.NewWriter(&buf)
|
|
for _, name := range []string{"Backup.sqlite", "Backup.sqlite-wal"} {
|
|
w, err := zw.Create(name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := w.Write(bytes.Repeat([]byte("s"), payload)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := zw.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
// 上限低于条目体积:必须报错,而且不留半个文件在盘上
|
|
if _, err := unpackArchive(zr, dir, payload-1); err == nil {
|
|
t.Error("条目超过上限时应当报错")
|
|
} else {
|
|
t.Logf("已拒绝: %v", err)
|
|
}
|
|
if left, _ := filepathGlob(dir); len(left) != 0 {
|
|
t.Errorf("被拒绝的条目留下了残留文件: %v", left)
|
|
}
|
|
|
|
// 放宽到够用:正常解出主库路径
|
|
dbPath, err := unpackArchive(zr, dir, payload)
|
|
if err != nil {
|
|
t.Fatalf("上限够用时不该报错: %v", err)
|
|
}
|
|
if !strings.HasSuffix(dbPath, "Backup.sqlite") {
|
|
t.Errorf("返回的主库路径不对: %s", dbPath)
|
|
}
|
|
}
|
|
|
|
// filepathGlob 列出 dir 下的文件(测试辅助,避免为上一条再引一个包)。
|
|
func filepathGlob(dir string) ([]string, error) {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []string
|
|
for _, e := range entries {
|
|
out = append(out, e.Name())
|
|
}
|
|
return out, nil
|
|
}
|