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 再用应用侧读查询真打一遍, 迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
118 lines
3.6 KiB
Go
118 lines
3.6 KiB
Go
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()))
|
||
}
|
||
}
|