安全与可靠性: 修复配额并发、投票事务、密码授权与导出完整性
This commit is contained in:
+21
-4
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user