Files
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

118 lines
3.6 KiB
Go
Raw Permalink 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 (
"context"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"rond-with-you/internal/auth"
)
// enableTOTP 给 fixture 账号开上两步验证,返回明文秘钥;收尾时关掉并把计数器还原。
func enableTOTP(t *testing.T, f *loginFixture) string {
t.Helper()
ctx := context.Background()
secret, err := auth.GenerateTOTPSecret()
if err != nil {
t.Fatalf("生成秘钥失败: %v", err)
}
if err := f.srv.auth.SetTOTPSecret(ctx, f.uid, secret); err != nil {
t.Fatalf("写秘钥失败: %v", err)
}
if err := f.srv.auth.EnableTOTP(ctx, f.uid); err != nil {
t.Fatalf("开启两步验证失败: %v", err)
}
f.srv.loginGuard.Reset()
t.Cleanup(func() {
f.srv.auth.DisableTOTP(ctx, f.uid)
f.srv.loginGuard.Reset()
})
return secret
}
// pending2FA 签发一张当前账号的中间令牌(等价于口令已经输对了)。
func pending2FA(t *testing.T, f *loginFixture) string {
t.Helper()
u, err := f.srv.auth.UserByID(context.Background(), f.uid)
if err != nil {
t.Fatalf("取账号失败: %v", err)
}
return f.srv.auth.SignPending2FA(u.ID, u.Hash)
}
// attempt2FA 走一次真实的验证码提交。
func attempt2FA(t *testing.T, f *loginFixture, pending, code string) *httptest.ResponseRecorder {
t.Helper()
form := url.Values{"pending": {pending}, "code": {code}}
r := httptest.NewRequest("POST", "/admin/login/twofa", strings.NewReader(form.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
f.srv.login2FA(w, r)
return w
}
// S7 回归:验证码这一环必须限速。
//
// 中间令牌有 5 分钟有效期,而口令那一环的短锁在「口令正确」时就被清掉了,
// 所以攻击者只要知道口令,就能拿着一张令牌在 5 分钟里无限试 6 位码。
func TestLogin2FARateLimited(t *testing.T) {
f := newLoginFixture(t)
secret := enableTOTP(t, f)
pending := pending2FA(t, f)
// 连错到阈值:阈值内只提示「验证码不正确」(手滑不该被罚),过了阈值才上锁
lockedAt := 0
for i := 1; i <= 8; i++ {
rec := attempt2FA(t, f, pending, "000000")
if strings.Contains(rec.Body.String(), "频繁") {
lockedAt = i
break
}
if !strings.Contains(rec.Body.String(), "验证码不正确") {
t.Fatalf("第 %d 次错码的提示不对: %s", i, firstLine(rec.Body.String()))
}
}
if lockedAt == 0 {
t.Fatal("连错 8 次也没触发短锁")
}
if lockedAt < 2 {
t.Errorf("第一次错码就上锁了,正常手滑会被误伤(lockedAt=%d)", lockedAt)
}
// 上锁之后,即使递上**正确**的码也必须被拦下——否则限速没生效
if code, err := auth.TOTPCode(secret, time.Now()); err == nil {
rec := attempt2FA(t, f, pending, code)
if !strings.Contains(rec.Body.String(), "频繁") {
t.Errorf("锁定期内递正确码没被拦住,仍走到了后续分支: %s", firstLine(rec.Body.String()))
}
} else {
t.Fatalf("算验证码失败: %v", err)
}
}
// S7 回归:同一个验证码不能换到第二个会话。
func TestLogin2FACodeIsSingleUse(t *testing.T) {
f := newLoginFixture(t)
secret := enableTOTP(t, f)
pending := pending2FA(t, f)
code, err := auth.TOTPCode(secret, time.Now())
if err != nil {
t.Fatalf("算验证码失败: %v", err)
}
rec := attempt2FA(t, f, pending, code)
if rec.Code != http.StatusSeeOther {
t.Fatalf("第一次用正确码应登录成功,实际 %d: %s", rec.Code, firstLine(rec.Body.String()))
}
rec = attempt2FA(t, f, pending, code)
if !strings.Contains(rec.Body.String(), "刚用过") {
t.Errorf("同一个码被重放了: %d %s", rec.Code, firstLine(rec.Body.String()))
}
}