fix anwser token and database issus
This commit is contained in:
+33
-49
@@ -49,13 +49,11 @@ import (
|
||||
// 用于防止把路由参数直接拼进 embed FS 路径时发生路径穿越(如 "../robots.txt")。
|
||||
var indexNowKeyPattern = regexp.MustCompile(`^[0-9a-f]{32}\.txt$`)
|
||||
|
||||
// CSRFUserSessionKey 是 CSRF token 绑定的会话键,必须与登录时写入的会话键一致
|
||||
// (见 route/auth/login.go 中 ctx.Session.Set("uid", ...))。flamego/csrf 默认使用
|
||||
// "userID",若不与之对齐,token 的 id 恒为 "0",任意匿名访客从公开页面取到的 token
|
||||
// 对全部已登录用户均有效,CSRF 退化为全站公开常量。
|
||||
//
|
||||
// 本常量同时被 csrf_binding_test.go 引用,作为单一事实来源:一旦被误改,回归测试会在
|
||||
// CI 失败,避免修复悄悄倒退。
|
||||
// CSRFUserSessionKey 是 CSRF token 绑定的会话键,必须与登录时写入的键一致
|
||||
// (见 route/auth/login.go 的 ctx.Session.Set("uid", ...))。flamego/csrf 默认用
|
||||
// "userID",不对齐则 token id 恒为 "0",匿名访客从公开页取到的 token 对所有已登录
|
||||
// 用户均有效,CSRF 退化为全站公开常量。
|
||||
// 本常量被 csrf_binding_test.go 引用作为单一事实来源,误改会让回归测试失败。
|
||||
const CSRFUserSessionKey = "uid"
|
||||
|
||||
// securityHeaders adds security-related HTTP headers to every response.
|
||||
@@ -80,9 +78,8 @@ func New() *flamego.Flame {
|
||||
}
|
||||
|
||||
// 自动封禁实例:整个请求链路共享同一个,供中间件与 f.NotFound 复用,
|
||||
// 避免对业务性 302→/(语言切换、登出、投票不存在等)误判为路径扫描。
|
||||
// 404 是否参与统计由 ban_track_notfound 控制(默认 false:正常业务一律放行,
|
||||
// 仅敏感扫描路径计数封禁,见 conf.Security.BanTrackNotFound)。
|
||||
// 避免对业务性 302→/(语言切换、登出等)误判为路径扫描。
|
||||
// 404 是否计入由 ban_track_notfound 控制(默认 false,仅敏感扫描路径计数)。
|
||||
ab := autoban.New(autoban.Options{
|
||||
Enabled: conf.Security.EnableAutoBan,
|
||||
TrackNotFound: conf.Security.BanTrackNotFound,
|
||||
@@ -98,10 +95,6 @@ func New() *flamego.Flame {
|
||||
return mail.SendSecurityAlertMail(ip, provider, proxied, paths, durationMinutes)
|
||||
},
|
||||
})
|
||||
// 需要输出详细日志时配置如下
|
||||
// if conf.App.Production {
|
||||
// flamego.SetEnv(flamego.EnvTypeProd)
|
||||
// }
|
||||
|
||||
templateFS, err := template.EmbedFS(templates.FS, ".", []string{".html"})
|
||||
if err != nil {
|
||||
@@ -134,11 +127,9 @@ func New() *flamego.Flame {
|
||||
sessioner := session.Sessioner(session.Options{
|
||||
Initer: initer,
|
||||
Config: sessionStorage,
|
||||
// 显式声明 cookie 安全属性,不依赖库默认值:
|
||||
// - HTTPOnly:禁止 JS 读取,降低 XSS 窃取会话的风险
|
||||
// - SameSite=Lax:阻断经典跨站表单 POST(CSRF 的主要防线之一)
|
||||
// - Secure:仅在站点以 https 对外服务时下发,避免 http 本地开发下 cookie 不生效。
|
||||
// 缺失 Secure 时,HSTS 尚未落地的首次访问可被主动 MITM 诱导明文请求窃取会话。
|
||||
// 显式声明 cookie 安全属性:HTTPOnly 防 XSS 窃取会话;SameSite=Lax 阻断跨站表单
|
||||
// POST;Secure 仅在 https 对外服务时下发,避免 http 本地开发下 cookie 不生效
|
||||
// (缺失 Secure 时,HSTS 尚未落地的首次访问可被 MITM 诱导明文请求窃取会话)。
|
||||
Cookie: session.CookieOptions{
|
||||
HTTPOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
@@ -227,11 +218,11 @@ func New() *flamego.Flame {
|
||||
_, _ = io.Copy(c.ResponseWriter(), fs)
|
||||
})
|
||||
|
||||
// IndexNow 密钥文件:static/index-now/<32 位十六进制>.txt,供 Bing / Yandex / Seznam
|
||||
// 等支持 IndexNow 的搜索引擎校验站点所有权(提交 URL 时的 keyLocation 指向此处)。
|
||||
// ⚠️ flamego 的 Static 中间件 Prefix 为 "/static",不会接管本路径,必须在此显式注册,
|
||||
// 否则请求会落到 f.NotFound 被 302 重定向到首页,搜索引擎校验必然失败。
|
||||
// 文件名用 indexNowKeyPattern 严格白名单校验,避免路径穿越读取 embed FS 内其它文件。
|
||||
// IndexNow 密钥文件:static/index-now/<32 位十六进制>.txt,供 Bing / Yandex 等
|
||||
// 搜索引擎校验站点所有权。
|
||||
// ⚠️ flamego Static 的 Prefix 是 "/static",不会接管本路径,必须显式注册,
|
||||
// 否则会落到 f.NotFound 被 302 到首页,校验必然失败。
|
||||
// 文件名经 indexNowKeyPattern 白名单校验,避免路径穿越读 embed FS 内其它文件。
|
||||
f.Get("/index-now/{key}", func(c context.Context) {
|
||||
key := c.Param("key")
|
||||
if !indexNowKeyPattern.MatchString(key) {
|
||||
@@ -282,7 +273,7 @@ func New() *flamego.Flame {
|
||||
f.Post("/follow-up/{followUpID}/answer", reqUserSignIn, form.Bind(form.AnswerFollowUpQuestion{}), question.AnswerFollowUp)
|
||||
}, question.Questioner)
|
||||
|
||||
// 新增:私聊路由(需要登录,使用 Token 标识对话)
|
||||
// 私聊路由(需登录,Token 标识对话)
|
||||
f.Group("/chat/{token}", func() {
|
||||
f.Get("", chat.ConversationPage)
|
||||
f.Post("/send", rateLimitMiddleware(20, 1*time.Minute), chat.SendMessage)
|
||||
@@ -300,16 +291,14 @@ func New() *flamego.Flame {
|
||||
f.Post("/submit", voteSubmitRateLimitMiddleware(20, 1*time.Minute), vote.Submit)
|
||||
})
|
||||
|
||||
// 公开页语言切换:把语言偏好写入 session(由 session 中间件随其 cookie 一并持久化)
|
||||
// 后跳回原页面。此前写独立 lang cookie 会被 session 中间件的 Set-Cookie 整体覆盖而失效,
|
||||
// 故改为托管在 session 中。仅允许站内相对路径,防开放重定向。
|
||||
// 公开页语言切换:语言偏好写入 session 后跳回原页面。此前写独立 lang cookie
|
||||
// 会被 session 中间件的 Set-Cookie 整体覆盖而失效,故托管在 session 中。
|
||||
f.Get("/i18n/{lang}", func(c context.Context) {
|
||||
lang := i18n.Resolve(c.Param("lang"))
|
||||
// 写入 session,避免被 session 中间件的 Set-Cookie 覆盖(匿名用户也走 session 中间件)。
|
||||
c.Session.Set("lang", lang)
|
||||
to := c.Query("to")
|
||||
// 仅允许站内相对路径,防开放重定向:
|
||||
// 拒绝空、非 "/" 开头、以 "//" 开头的协议相对路径,以及含反斜杠/控制字符的变体。
|
||||
// 仅允许站内相对路径,防开放重定向:拒绝空、非 "/" 开头、协议相对路径 "//",
|
||||
// 以及含反斜杠 / 控制字符的变体。
|
||||
if to == "" || !strings.HasPrefix(to, "/") ||
|
||||
strings.HasPrefix(to, "//") || strings.ContainsAny(to, "\\\x00\r\n\t") {
|
||||
to = "/"
|
||||
@@ -360,13 +349,10 @@ func New() *flamego.Flame {
|
||||
f.Post("/reset-quota", rateLimitMiddleware(20, 1*time.Minute), user.FeatureResetQuota)
|
||||
})
|
||||
})
|
||||
// 防骚扰设置:注册限制(harassment/update)与全局屏蔽词(blockwords/update)
|
||||
// 拆为两个独立入口。屏蔽词在提问/私聊/投票三个设置面板共用一份,保存后经
|
||||
// ?tab= 参数回跳到来源面板。
|
||||
// 防骚扰与全局屏蔽词拆为两个独立入口,避免同表单一次写两列时相互清空。
|
||||
// 保存后经 ?tab= 参数回跳到来源面板。
|
||||
f.Post("/harassment/update", form.Bind(form.UpdateHarassment{}), user.UpdateHarassment)
|
||||
f.Post("/blockwords/update", form.Bind(form.UpdateBlockWords{}), user.UpdateBlockWords)
|
||||
// 内容审核严格度:与屏蔽词一样在提问/私聊/投票三个面板共用一份,
|
||||
// 保存后经 ?tab= 参数回跳到来源面板。
|
||||
f.Post("/moderation-strictness/update", form.Bind(form.UpdateModerationStrictness{}), user.UpdateModerationStrictness)
|
||||
f.Post("/tags/update", form.Bind(form.UpdateTags{}), user.UpdateTags)
|
||||
// Logout 必须用 POST:GET 会留下 CSRF 风险(第三方图片/链接可触发退出登录)
|
||||
@@ -418,12 +404,11 @@ func New() *flamego.Flame {
|
||||
csrf.Csrfer(csrf.Options{
|
||||
Secret: conf.Server.XSRFKey,
|
||||
Header: "X-CSRF-Token",
|
||||
// ⚠️ 必须与 route/auth/login.go 中 ctx.Session.Set("uid", ...) 的 key 一致。
|
||||
// flamego/csrf 默认 SessionKey 为 "userID",若不显式指定,s.Get("userID")
|
||||
// 恒为 nil,token 内绑定的 id 恒为 "0" —— 由于校验是无状态 HMAC 重算
|
||||
// (不与 session 中存储的 token 比对),这会导致任意匿名访客从公开的
|
||||
// /login 页面取到的 token 对所有已登录用户均有效,CSRF 退化为全站公开常量。
|
||||
// 回归测试见 route/csrf_binding_test.go(引用 CSRFUserSessionKey)。
|
||||
// ⚠️ 必须与 login.go 的 ctx.Session.Set("uid", ...) 一致。flamego/csrf 默认
|
||||
// SessionKey 为 "userID",不显式指定则 s.Get("userID") 恒为 nil、token 内 id
|
||||
// 恒为 "0";校验是无状态 HMAC 重算(不与 session 内 token 比对),匿名访客从
|
||||
// 公开页取到的 token 会对所有已登录用户有效,CSRF 退化为全站公开常量。
|
||||
// 回归测试见 route/csrf_binding_test.go。
|
||||
SessionKey: CSRFUserSessionKey,
|
||||
}),
|
||||
template.Templater(template.Options{
|
||||
@@ -433,13 +418,12 @@ func New() *flamego.Flame {
|
||||
context.Contexter(),
|
||||
)
|
||||
f.NotFound(func(ctx flamego.Context) {
|
||||
// 真正的 404 探测在此记录(仅此处,不把业务 302→/ 误判为扫描)。
|
||||
// 注意:flamego 的 NotFound 会先写入 404 状态再 Redirect 到 "/",
|
||||
// 因此这里在 Redirect 之前记录,确保状态为 404。
|
||||
// 键必须与 autobanMiddleware 中的 IsBanned/Record 保持一致(同样经 LimitKey 聚合),
|
||||
// 否则 404 计数会记在另一个键上,导致封禁永不生效。
|
||||
// ⚠️ 该 404 计数受 ban_track_notfound 配置控制,默认关闭(正常业务一律放行,
|
||||
// 仅敏感扫描路径计数封禁);Record 内部对未开启的 KindNotFound 直接忽略。
|
||||
// 真正的 404 探测在此记录(不把业务 302→/ 误判为扫描)。flamego 的 NotFound 会先
|
||||
// 写 404 状态再 Redirect,故须在 Redirect 前记录。
|
||||
// 键必须与 autobanMiddleware 的 IsBanned/Record 一致(同样经 LimitKey 聚合),
|
||||
// 否则计数落在另一个键上,封禁永不生效。
|
||||
// ⚠️ 计数受 ban_track_notfound 控制,默认关闭;Record 内部会忽略未开启的
|
||||
// KindNotFound。
|
||||
if ip := shared.LimitKey(shared.ResolveClientIP(ctx.Request().Request)); ip != "" {
|
||||
ab.Record(ip, ctx.Request().URL.Path, autoban.KindNotFound)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user