fix(security): 修复安全审计的 P1/P2/P3 项,含全局复查新发现的两处越权与外跳
P1: - 外链图片:markdown 渲染层只放行站内与自有 CDN 图片。配置里的 image_bucket_cdn_host 常写成 host/path,统一归一化后再比对,避免把自家图一起剥掉 - 密码:页面/提问密码最小 6 位;密码比对挪到验证码之后;页面密码失败锁定 5 次/10 分钟 - 客户端 IP:CF-Connecting-IP / EO-Client-IP 只在 TCP 对端可信(回环或 trusted_proxies) 时采信,公网对端带来的权威头一律忽略 P2/P3: - 跳转参数收敛到 shared.SafeRedirectTarget:拒反斜杠与控制字符(url.Parse 会把 %0d%0a 解成 CRLF,伪造 Location),最后再用 path.Clean 验一次前缀 - 注册不再暴露「邮箱已注册 / 已停用」:与全新注册共用同一个成功出口,只在日志留痕 - CSP:默认 enforce,白名单由配置派生,覆盖 umami(运行时注入)、Turnstile 与 按 sentry_dsn 条件放行的 Sentry CDN - /user 与 /_ 下发 no-store;补 X-XSS-Protection、Permissions-Policy,并把安全头前移到 静态资源中间件之前(原先静态文件拿不到 nosniff/HSTS) - 封禁事实落库 ban_records;429 统一带 Retry-After 与本地化文案 - token 比较改常量时间;探索页页码上限;JSON 响应显式 Content-Type 全局复查补的两处: - 私聊 SendMessage 的 POST 缺 conv.OwnerID 校验,被拉黑者可换 domain 继续发消息 - 页面密码 next 与登录 to 同源的反斜杠外跳 验收:verify_p1_fixes 4/4、verify_round2 19/19、verify_page_next 6/6、 verify_chat_owner 3/3(含变异对照);单测与 go build ./... 全通过。
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
// Copyright 2026 tamakyi. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestBanRecordStore 覆盖自动封禁账本的写入与检索。
|
||||
//
|
||||
// 这张表不参与判定(IsBanned 只读内存),所以测试只锁两件事:
|
||||
// ① 记录能落库并按时间倒序取回;② 到期时间与时长原样保留 ——
|
||||
// 事后排查全依赖这两个字段回答「封了多久、什么时候解」。
|
||||
func TestBanRecordStore(t *testing.T) {
|
||||
database, cleanup := newTestDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Cleanup(func() {
|
||||
if err := cleanup("ban_records"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
store := NewBanRecordsStore(database)
|
||||
now := time.Now()
|
||||
|
||||
rows := []*BanRecord{
|
||||
{BanKey: "1.2.3.4", Reason: "敏感路径探测", DurationMinutes: 60, ExpiresAt: now.Add(time.Hour), CreatedAt: now.Add(-2 * time.Hour)},
|
||||
{BanKey: "2001:db8::/64", Reason: "蜜罐提交凭据", DurationMinutes: 1440, ExpiresAt: now.Add(24 * time.Hour), CreatedAt: now.Add(-10 * time.Minute)},
|
||||
}
|
||||
for _, r := range rows {
|
||||
require.NoError(t, store.Create(ctx, r))
|
||||
}
|
||||
|
||||
got, err := store.ListSince(ctx, now.Add(-3*time.Hour), 10)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
|
||||
// 时间倒序:最新的一条在最前
|
||||
require.Equal(t, "2001:db8::/64", got[0].BanKey)
|
||||
require.Equal(t, "蜜罐提交凭据", got[0].Reason)
|
||||
require.Equal(t, 1440, got[0].DurationMinutes)
|
||||
require.False(t, got[0].ExpiresAt.IsZero(), "到期时间必须保留,否则无法回答「什么时候解」")
|
||||
|
||||
// 时间窗过滤生效
|
||||
recent, err := store.ListSince(ctx, now.Add(-30*time.Minute), 10)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, recent, 1)
|
||||
require.Equal(t, "2001:db8::/64", recent[0].BanKey)
|
||||
}
|
||||
Reference in New Issue
Block a user