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

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
+59
View File
@@ -0,0 +1,59 @@
package web
import (
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"
)
// deadlineRecorder 冒充 http.response:只有实现了 SetWriteDeadline,
// 才看得出 http.NewResponseController 有没有被中间件挡住。
type deadlineRecorder struct {
*httptest.ResponseRecorder
deadline time.Time
got bool
}
func (d *deadlineRecorder) SetWriteDeadline(t time.Time) error {
d.deadline, d.got = t, true
return nil
}
// P4 回归:gzipWriter 必须实现 Unwrap()。少了它,被 gzip 中间件包住的请求调
// http.NewResponseController(...).SetWriteDeadline/Flush 只会拿到 ErrNotSupported。
func TestGzipWriterUnwrapsToUnderlyingWriter(t *testing.T) {
rec := &deadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
gw := &gzipWriter{ResponseWriter: rec}
if err := http.NewResponseController(gw).SetWriteDeadline(time.Now().Add(time.Minute)); err != nil {
t.Fatalf("ResponseController 没能穿过 gzipWriter: %v", err)
}
if !rec.got {
t.Error("SetWriteDeadline 没有落到真正的 ResponseWriter 上")
}
}
// P4 回归:导出是「一次性大响应」,得带上 Content-Length(下载进度条、
// 也顺带锁住「不再依赖隐式 200」),并把写死线放宽到全站 120s 之外。
func TestExportSetsContentLengthAndExtendsWriteDeadline(t *testing.T) {
f := newMovementFixture(t)
rec := &deadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
req := httptest.NewRequest("GET", "/admin/export/rondbackup", nil).WithContext(f.ctx)
f.srv.exportRondbackup(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("导出返回 %d:%s", rec.Code, firstLine(rec.Body.String()))
}
if cl := rec.Header().Get("Content-Length"); cl != strconv.Itoa(rec.Body.Len()) {
t.Errorf("Content-Length=%q,实际写出 %d 字节", cl, rec.Body.Len())
}
if ct := rec.Header().Get("Content-Type"); ct != "application/octet-stream" {
t.Errorf("Content-Type 不对: %q", ct)
}
if !rec.got {
t.Error("导出没有延长写死线(慢链路下会被全站 120s 截断)")
}
}