安全: 修复页面密码零值绕过,导出不再泄露投票者 IP
页面密码(P0,2026-11-05 复核发现) - 旧实现 `ver, _ := Session.Get(key).(uint); if ver != user.PagePasswordVer` 把「session 里没有授权记录」(断言失败得 0)与「新增列默认值 0」判成相等, 升级瞬间所有存量页面密码静默失效:匿名访客不带 cookie 即可读箱主页。 - 收口为 pagePasswordAuthorized:缺 key 一律视为未授权, key 名由 pagePassKey 统一生成,Pager / ListAPI / AskStatusAPI 三处共用。 - 新增迁移 v6 回填 page_password_ver / ask_password_ver(仅仍为 0 且已设密码的行, 幂等),让模型语义与「改过一次密码」一致,并作废升级前的布尔授权。 - AskStatusAPI 的 L4 查询补日志(保持与 Pager 一致的 fail-open 口径)。 导出(P1) - JSON 导出的投票提交不再输出 voter_ip。库里照常保留, 防刷、取证与站方 CLI 处置不受影响,只是不进箱主的自助导出文件。 测试 - route/question/page_password_test.go:8 个用例钉住零值/类型不符/轮换失效等边界。 - internal/db/page_password_migration_test.go:回填只碰该碰的行且幂等。 验证:WSL2 Debian + PostgreSQL 全量测试通过;隔离实例实测 ver=0 匿名访问返回密码页、API 401,输对密码后放行、改密后旧会话立即失效, 删除迁移账本后重启可正确回填。
This commit is contained in:
@@ -7,7 +7,6 @@ package question
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -106,23 +105,23 @@ func AskStatusAPI(ctx context.Context) error {
|
||||
// 否则持有正确问题 token 的请求者可以绕过页面访问边界,取得回答状态、
|
||||
// 创建时间和前 60 字摘要。
|
||||
// 两者都返回「用户不存在」以隐藏真实原因(不暴露处置状态与密码设置)。
|
||||
if level, _, err := db.Restrictions.ActiveLevel(ctx.Request().Context(), owner.ID); err == nil && level == db.RestrictionL4 {
|
||||
emptyResult := func() error {
|
||||
out := make([]askStatusResult, 0, len(req.Items))
|
||||
for _, it := range req.Items {
|
||||
out = append(out, askStatusResult{ID: it.ID})
|
||||
}
|
||||
return ctx.JSON(map[string]interface{}{"items": out})
|
||||
}
|
||||
if owner.PagePassword != "" && !(ctx.IsLogged && ctx.User.ID == owner.ID) {
|
||||
passKey := fmt.Sprintf("page_pass_%d", owner.ID)
|
||||
sessionVer, _ := ctx.Session.Get(passKey).(uint)
|
||||
if sessionVer != owner.PagePasswordVer {
|
||||
out := make([]askStatusResult, 0, len(req.Items))
|
||||
for _, it := range req.Items {
|
||||
out = append(out, askStatusResult{ID: it.ID})
|
||||
}
|
||||
return ctx.JSON(map[string]interface{}{"items": out})
|
||||
}
|
||||
// 处置查询失败时不拦(fail-open),与 Pager / ListAPI 的既定口径一致:
|
||||
// 这一层不是内容外传通道,DB 异常时再 fail-closed 只会让所有提问箱一起白屏。
|
||||
// 与它们唯一的差别是这里补了日志,不再静默吞掉错误。
|
||||
if level, _, err := db.Restrictions.ActiveLevel(ctx.Request().Context(), owner.ID); err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to query user restriction for ask status")
|
||||
} else if level == db.RestrictionL4 {
|
||||
return emptyResult()
|
||||
}
|
||||
if !pagePasswordAuthorized(ctx, owner) {
|
||||
return emptyResult()
|
||||
}
|
||||
|
||||
// 收集合法 ID 并一次查回(IN 查询,避免逐条 N+1)。
|
||||
|
||||
Reference in New Issue
Block a user