Files
rond-with-you/internal/rond/limit_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

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
}