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

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
+81
View File
@@ -0,0 +1,81 @@
package web
import (
"context"
"os"
"regexp"
"strings"
"testing"
)
// TestAggHelpersPropagateErrors 是 D1 的行为守卫:四个「重算聚合 / 写标签」的辅助函数
// 必须把数据库错误返回给调用方。
//
// 它们原先的签名是 `(isHome, isWork bool)` / 无返回值,Scan 的错误被直接丢掉——
// 于是「查询炸了」和「查到了 false/false」在调用方看来一模一样:到访会被写得没有归属活动,
// 计数会静默地停在旧值上。用一个已取消的 context 就能逼出这一层错误。
func TestAggHelpersPropagateErrors(t *testing.T) {
f := newMovementFixture(t)
// 已取消的 context:任何一次 Query/Exec 都会立刻以 context.Canceled 返回。
ctx, cancel := context.WithCancel(f.ctx)
cancel()
if err := f.srv.refreshDatasetAgg(ctx, f.pg, f.dsID); err == nil {
t.Error("refreshDatasetAgg 吞掉了错误:ctx 已取消却返回 nil")
}
if err := f.srv.refreshPlaceAgg(ctx, f.pg, 1); err == nil {
t.Error("refreshPlaceAgg 吞掉了错误:ctx 已取消却返回 nil")
}
if err := f.srv.saveVisitTags(ctx, f.pg, f.dsID, 1, nil); err == nil {
t.Error("saveVisitTags 吞掉了错误:ctx 已取消却返回 nil")
}
if _, _, err := f.srv.activityFlags(ctx, f.pg, 1); err == nil {
t.Error("activityFlags 吞掉了错误:ctx 已取消却返回 nil")
}
}
// TestNoAggCallSiteSwallowsError 是 D1 的静态守卫:这三个辅助函数「返回 error」只有在
// 每个调用点都接住它时才有意义。裸调用(语句形式、丢掉返回值)会随第一个新加的调用点回归,
// 而且编译器不会拦——这里按行扫描源码,把裸调用钉死。
func TestNoAggCallSiteSwallowsError(t *testing.T) {
// 只匹配「整行就是一次调用」的裸语句,合法写法前面一定还有 `if err := `、`return `、
// `logAggFailure(` 等前缀,因此不会被误伤。
bare := regexp.MustCompile(`^\s*s\.(refreshDatasetAgg|refreshPlaceAgg|saveVisitTags)\(`)
for _, name := range []string{
"handlers_edit.go",
"handlers_photos.go",
"fillup.go",
} {
path := pkgFile(t, name)
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("读 %s 失败: %v", path, err)
}
for i, line := range strings.Split(string(data), "\n") {
if bare.MatchString(line) {
t.Errorf("%s:%d 裸调用吞掉了 error:%s", name, i+1, strings.TrimSpace(line))
}
}
}
}
// TestAggHelpersHaveErrorReturn 钉住签名本身:如果谁把它们改回无返回值,
// 上面两条用例会失去意义(编译期就不会再有 error 可断言)。
func TestAggHelpersHaveErrorReturn(t *testing.T) {
data, err := os.ReadFile(pkgFile(t, "handlers_edit.go"))
if err != nil {
t.Fatalf("读 handlers_edit.go 失败: %v", err)
}
src := string(data)
for _, sig := range []string{
"func (s *Server) saveVisitTags(ctx context.Context, ex execer, datasetID int64, visitSrc int, tagIDs []string) error",
"func (s *Server) refreshPlaceAgg(ctx context.Context, ex execer, placeID int64) error",
"func (s *Server) refreshDatasetAgg(ctx context.Context, ex execer, datasetID int64) error",
} {
if !strings.Contains(src, sig) {
t.Errorf("签名被改掉了(不再返回 error):%s", sig)
}
}
}