审计修复:后端(安全 / 数据一致性 / 性能)

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:
tamakyi
2026-09-24 20:37:16 +08:00
parent 78865fc736
commit b4c2a8452f
70 changed files with 5417 additions and 360 deletions
+147
View File
@@ -0,0 +1,147 @@
package auth
import (
"strings"
"testing"
"time"
)
// TestGuardLocksAfterThreshold 前几次手滑不该被罚,到阈值才开始锁。
func TestGuardLocksAfterThreshold(t *testing.T) {
g := NewGuard()
base := time.Date(2026, 9, 21, 10, 0, 0, 0, time.UTC)
g.now = func() time.Time { return base }
for i := 1; i < guardFreeTries; i++ {
if wait := g.Fail("admin"); wait != 0 {
t.Fatalf("第 %d 次失败就被锁了 %v,阈值应当是 %d", i, wait, guardFreeTries)
}
if d := g.RetryAfter("admin"); d != 0 {
t.Fatalf("第 %d 次失败后不该有等待时间,实际 %v", i, d)
}
}
wait := g.Fail("admin")
if wait <= 0 {
t.Fatalf("第 %d 次失败应当上锁", guardFreeTries)
}
if wait > guardBaseLock {
t.Errorf("首次锁定时长 %v 超过了基准 %v", wait, guardBaseLock)
}
if d := g.RetryAfter("admin"); d != wait {
t.Errorf("RetryAfter=%v 与 Fail 返回的 %v 不一致", d, wait)
}
t.Logf("第 %d 次失败锁 %v", guardFreeTries, wait)
}
// TestGuardBackoffGrowsAndCaps 继续失败要退避得更久,但封顶。
func TestGuardBackoffGrowsAndCaps(t *testing.T) {
g := NewGuard()
base := time.Date(2026, 9, 21, 10, 0, 0, 0, time.UTC)
g.now = func() time.Time { return base }
var prev time.Duration
for i := 0; i < guardFreeTries+12; i++ {
wait := g.Fail("admin")
if wait < prev {
t.Fatalf("第 %d 次失败的等待时间 %v 比上一次 %v 还短", i+1, wait, prev)
}
if wait > guardMaxLock {
t.Fatalf("第 %d 次失败锁了 %v,超过上限 %v", i+1, wait, guardMaxLock)
}
prev = wait
}
if prev != guardMaxLock {
t.Errorf("连续失败后应当封顶在 %v,实际 %v", guardMaxLock, prev)
}
}
// TestGuardReleasesAfterWait 时间走够之后要放行,否则就是永久锁死自己。
func TestGuardReleasesAfterWait(t *testing.T) {
g := NewGuard()
base := time.Date(2026, 9, 21, 10, 0, 0, 0, time.UTC)
now := base
g.now = func() time.Time { return now }
var wait time.Duration
for i := 0; i < guardFreeTries; i++ {
wait = g.Fail("admin")
}
if wait <= 0 {
t.Fatal("到阈值却没上锁")
}
now = base.Add(wait - time.Millisecond)
if d := g.RetryAfter("admin"); d <= 0 {
t.Errorf("还差 1ms 到期,应当仍在锁内,实际 %v", d)
}
now = base.Add(wait)
if d := g.RetryAfter("admin"); d != 0 {
t.Errorf("到期后应当放行,实际还要等 %v", d)
}
}
// TestGuardSucceedClears 登录成功要把计数清零,别让几天前的失败把下次正常登录挡住。
func TestGuardSucceedClears(t *testing.T) {
g := NewGuard()
base := time.Date(2026, 9, 21, 10, 0, 0, 0, time.UTC)
g.now = func() time.Time { return base }
for i := 0; i < guardFreeTries; i++ {
g.Fail("admin")
}
if g.RetryAfter("admin") <= 0 {
t.Fatal("应当处于锁定状态")
}
g.Succeed("admin")
if d := g.RetryAfter("admin"); d != 0 {
t.Errorf("成功后不该还有等待时间,实际 %v", d)
}
// 清零之后又要重新攒满阈值才会锁
for i := 1; i < guardFreeTries; i++ {
if wait := g.Fail("admin"); wait != 0 {
t.Errorf("清零后第 %d 次失败不该上锁,实际 %v", i, wait)
}
}
}
// TestGuardSeparatesKeys 不同用户名各算各的。
func TestGuardSeparatesKeys(t *testing.T) {
g := NewGuard()
base := time.Date(2026, 9, 21, 10, 0, 0, 0, time.UTC)
g.now = func() time.Time { return base }
for i := 0; i < guardFreeTries; i++ {
g.Fail("admin")
}
if g.RetryAfter("admin") <= 0 {
t.Fatal("admin 应当被锁")
}
if d := g.RetryAfter("someone-else"); d != 0 {
t.Errorf("另一个用户名不该受影响,实际 %v", d)
}
}
// TestRandomPassword 确认初始口令有足够熵、字符集不含易混字符。
func TestRandomPassword(t *testing.T) {
const n = 16
a, err := RandomPassword(n)
if err != nil {
t.Fatal(err)
}
b, err := RandomPassword(n)
if err != nil {
t.Fatal(err)
}
if len(a) != n || len(b) != n {
t.Fatalf("长度不对: %d / %d(应为 %d)", len(a), len(b), n)
}
if a == b {
t.Error("两次生成的随机口令相同")
}
if strings.ContainsAny(a, "0O1Il") {
t.Errorf("口令含易混字符: %s", a)
}
if _, err := RandomPassword(0); err != nil {
t.Errorf("长度 0 不该报错: %v", err)
}
}