安全与可靠性: 修复配额并发、投票事务、密码授权与导出完整性

This commit is contained in:
tamakyi
2026-11-05 12:17:05 +08:00
parent 7636456d4e
commit 25aee35030
18 changed files with 1002 additions and 194 deletions
+21 -4
View File
@@ -83,10 +83,12 @@ func Pager(ctx context.Context) {
// 与背景图、颜色同属箱主个性化配置;/user 后台由 Contexter 按登录用户自己的配置注入。
// 空串 = 不加载主题 css(站点默认就是基础外观时省掉一次请求)。
ctx.Data["ThemeSlug"] = theme.Resolve(pageUser.Theme)
// 页面密码检查
// 页面密码检查:session 中保存授权时的 PagePasswordVer 版本号,
// 与当前版本号比对,修改密码后旧授权立即失效(SEC-06)。
if pageUser.PagePassword != "" && !(ctx.IsLogged && ctx.User.ID == pageUser.ID) {
passKey := fmt.Sprintf("page_pass_%d", pageUser.ID)
if authorized, _ := ctx.Session.Get(passKey).(bool); !authorized {
sessionVer, _ := ctx.Session.Get(passKey).(uint)
if sessionVer != pageUser.PagePasswordVer {
// 记录原始请求地址(含 query,如带 token 的单题链接),验证通过后跳回此处而非默认首页
ctx.Data["Next"] = ctx.Request().URL.RequestURI()
ctx.Data["PageUser"] = pageUser
@@ -278,7 +280,8 @@ func VerifyPagePassword(ctx context.Context) {
if pageUser.CheckPagePassword(password) {
pagePasswordGuard.Reset(guardKey)
passKey := fmt.Sprintf("page_pass_%d", pageUser.ID)
ctx.Session.Set(passKey, true)
// 保存当前密码版本号到 session,修改密码后旧授权自动失效(SEC-06)
ctx.Session.Set(passKey, pageUser.PagePasswordVer)
// 验证成功后跳回原始页面(如带 token 的单题链接),避免一律回到首页。
// 必须走 shared.SafeRedirectTarget:这里原先自己判「以 / 开头、非 //、不含 ://」,
// 但没拒反斜杠 —— `next=/\evil.example` 会被浏览器当成 https://evil.example/。
@@ -330,7 +333,8 @@ func ListAPI(ctx context.Context) error {
}
if pageUser.PagePassword != "" && !(ctx.IsLogged && ctx.User.ID == pageUser.ID) {
passKey := fmt.Sprintf("page_pass_%d", pageUser.ID)
if authorized, _ := ctx.Session.Get(passKey).(bool); !authorized {
sessionVer, _ := ctx.Session.Get(passKey).(uint)
if sessionVer != pageUser.PagePasswordVer {
return ctx.JSONError(40100, i18n.T(ctx.Lang(), "password.desc"))
}
}
@@ -471,6 +475,9 @@ func New(ctx context.Context, f form.NewQuestion, pageUser *db.User, recaptcha r
todayCount, err := db.Users.CountTodayMessagesFromUser(ctx.Request().Context(), pageUser.ID, ctx.User.ID)
if err != nil {
logrus.WithError(err).Error("Failed to count today messages")
ctx.SetInternalError(f)
ctx.Success("question/list")
return
}
if todayCount >= int64(pageUser.DailyMessageLimit) {
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "question.errChatDailyLimit", pageUser.DailyMessageLimit))
@@ -558,6 +565,11 @@ func New(ctx context.Context, f form.NewQuestion, pageUser *db.User, recaptcha r
f.Content,
)
if err != nil {
if errors.Is(err, db.ErrDailyMessageLimit) {
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "question.errChatDailyLimit", pageUser.DailyMessageLimit))
ctx.Redirect("/_/" + pageUser.Domain)
return
}
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to create conversation with first message")
ctx.SetInternalError(f)
ctx.Success("question/list")
@@ -686,6 +698,11 @@ func New(ctx context.Context, f form.NewQuestion, pageUser *db.User, recaptcha r
IsPrivate: isPrivate,
})
if err != nil {
if errors.Is(err, db.ErrFeatureQuotaReached) {
ctx.SetErrorFlash(i18n.FeatureClosedMessage(ctx.Lang(), db.FeatureQuestion, db.FeatureReasonQuotaReached))
ctx.Redirect("/_/" + pageUser.Domain)
return
}
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to create new question")
ctx.SetInternalError(f)
ctx.Success("question/list")