修复: 评论组件泄露私密 token、cwebp 无超时、选项长度与去重
Waline(安全) - item.html 的 path 从 window.location.href 改为剔除 ?t= 后的 URL。 私密题 /_/<domain>/<id>?t=<token> 的钥匙原本会整条发往 Waline 服务端 并落进其访问日志。只删 t、保留其他参数,公开题的评论线程键不变。 cwebp(可靠性) - ConvertGifToAnimatedWebP 改用 exec.CommandContext + 30s 上限。 原来裸 exec.Command 无超时,畸形/超大 GIF 会把子进程和上传 goroutine 一起挂住。 超时杀进程并记 Warn,调用方按既有逻辑回退为原 GIF,不影响上传成功。 投票选项长度(正确性) - vote_options.content 由 varchar(200) 放宽到 300,与配置默认值一致。 此前 201–300 字的选项通过校验后在写库阶段失败。 - 新增 db.MaxVoteOptionLen;voteOptionMaxLen() 取配置与存储上限的较小值, 校验与前端 maxlength 同源,避免配置调大后再次越界。 多选 checkbox 去重(正确性) - 抽出 appendUniqueValue,预置值与「其他」文本共用同一 seen 集合。 此前重复提交同名字段会让同一选项在统计里被计多次。 测试 - vote_dedupe_test.go:去重与顺序。 - vote_option_len_test.go:配置小于/等于/大于存储上限三种情形。 - TestVoteOptionMaxLengthRoundTrip:300 个多字节字符存回不截断。 - TestVoteOptionColumnWidens:把列改回 200 后 AutoMigrate 能扩回 300(老库升级路径)。 验证:WSL2 Debian + PostgreSQL 全量 go test ./... 27 个包全绿; 隔离实例渲染单题页确认新的 path 逻辑。
This commit is contained in:
+20
-7
@@ -36,6 +36,19 @@ var voteTextRepliesCache = cache.NewTTLCache(64)
|
||||
|
||||
const voteTextRepliesCacheTTL = 30 * time.Second
|
||||
|
||||
// voteOptionMaxLen 是选项输入的实际上限。
|
||||
//
|
||||
// 配置项 vote_option_len 只保证 ≥1,可以调得比数据库列还大;而 vote_options.content
|
||||
// 是 varchar(300),超出的内容会被数据库拒绝(PostgreSQL 报错、MySQL 严格模式报错)。
|
||||
// 取「配置值」与「存储上限」的较小值,保证"校验通过"等于"能存下",
|
||||
// 也让前端输入框的 maxlength 与实际可保存长度一致。
|
||||
func voteOptionMaxLen() int {
|
||||
if conf.Limits.VoteOptionLen < db.MaxVoteOptionLen {
|
||||
return conf.Limits.VoteOptionLen
|
||||
}
|
||||
return db.MaxVoteOptionLen
|
||||
}
|
||||
|
||||
// ---------------- 表单解析 ----------------
|
||||
|
||||
// voteFormFieldInput 表单字段的前端输入结构(vote-form.html 中的 JS 数组)。
|
||||
@@ -244,8 +257,8 @@ func parseOptions(ctx context.Context, form *voteForm, voteType string, maxChoic
|
||||
if o == "" {
|
||||
continue
|
||||
}
|
||||
if len([]rune(o)) > conf.Limits.VoteOptionLen {
|
||||
return errors.New(i18n.T(ctx.Lang(), "vote.err.optionTooLong", conf.Limits.VoteOptionLen))
|
||||
if len([]rune(o)) > voteOptionMaxLen() {
|
||||
return errors.New(i18n.T(ctx.Lang(), "vote.err.optionTooLong", voteOptionMaxLen()))
|
||||
}
|
||||
// 选项内容审核(fail-closed):草稿跳过,发布时统一审核
|
||||
if !draft {
|
||||
@@ -348,8 +361,8 @@ func parseFormFields(ctx context.Context, form *voteForm, draft bool) error {
|
||||
return errors.New(i18n.T(ctx.Lang(), "vote.err.fieldReservedOption", i+1, label))
|
||||
}
|
||||
// 长度与单选/多选选项同规则(前端 maxlength 同源,这里补后端兜底校验)
|
||||
if len([]rune(o)) > conf.Limits.VoteOptionLen {
|
||||
return errors.New(i18n.T(ctx.Lang(), "vote.err.optionTooLong", conf.Limits.VoteOptionLen))
|
||||
if len([]rune(o)) > voteOptionMaxLen() {
|
||||
return errors.New(i18n.T(ctx.Lang(), "vote.err.optionTooLong", voteOptionMaxLen()))
|
||||
}
|
||||
}
|
||||
if len(opts) < 1 {
|
||||
@@ -516,7 +529,7 @@ func VoteList(ctx context.Context) {
|
||||
func VoteNew(ctx context.Context) {
|
||||
ctx.Data["Title"] = "新建投票"
|
||||
ctx.Data["IsEdit"] = false
|
||||
ctx.Data["MaxVoteOptionLen"] = conf.Limits.VoteOptionLen // 选项输入框 maxlength 同源(vote-form.html JS)
|
||||
ctx.Data["MaxVoteOptionLen"] = voteOptionMaxLen() // 选项输入框 maxlength 同源(vote-form.html JS)
|
||||
// 默认投票类型为表单收集(用户要求);同时给定模板零值兜底,避免切到多选时 max_choices 显示 0。
|
||||
ctx.Data["Vote"] = &db.Vote{
|
||||
Type: db.VoteTypeForm,
|
||||
@@ -534,7 +547,7 @@ func VoteNew(ctx context.Context) {
|
||||
// 错误提示直接写 c.Data 而非 session flash:本项目的 flash 在请求开始时就已读进 c.Data,
|
||||
// 之后写入 session 的 flash 本次渲染读不到,还会残留到下次请求造成「陈旧错误」串页。
|
||||
func renderVoteFormWithInput(ctx context.Context, isEdit bool, voteID uint, flashMsg string, internal bool) {
|
||||
ctx.Data["MaxVoteOptionLen"] = conf.Limits.VoteOptionLen // 选项输入框 maxlength 同源(vote-form.html JS)
|
||||
ctx.Data["MaxVoteOptionLen"] = voteOptionMaxLen() // 选项输入框 maxlength 同源(vote-form.html JS)
|
||||
get := func(n string) string { return strings.TrimSpace(ctx.Request().FormValue(n)) }
|
||||
title := get("title")
|
||||
description := get("description")
|
||||
@@ -674,7 +687,7 @@ func VoteEdit(ctx context.Context) {
|
||||
|
||||
ctx.Data["Vote"] = vote
|
||||
ctx.Data["IsEdit"] = true
|
||||
ctx.Data["MaxVoteOptionLen"] = conf.Limits.VoteOptionLen // 选项输入框 maxlength 同源(vote-form.html JS)
|
||||
ctx.Data["MaxVoteOptionLen"] = voteOptionMaxLen() // 选项输入框 maxlength 同源(vote-form.html JS)
|
||||
ctx.Data["Title"] = "编辑投票"
|
||||
// 草稿保存成功仅通过本次 URL 参数驱动统一 alert,不使用 session flash。
|
||||
if ctx.Query("saved") == "1" {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tamakyi/TamaBox/internal/conf"
|
||||
"github.com/tamakyi/TamaBox/internal/db"
|
||||
)
|
||||
|
||||
// TestVoteOptionMaxLen 钉住「配置值不能超过存储上限」这条约束。
|
||||
//
|
||||
// 背景:conf 的 vote_option_len 只校验 ≥1,可以填得比 vote_options.content
|
||||
// 的列宽还大;那样校验会放行,写库才失败(PostgreSQL 直接报错)。
|
||||
func TestVoteOptionMaxLen(t *testing.T) {
|
||||
original := conf.Limits.VoteOptionLen
|
||||
t.Cleanup(func() { conf.Limits.VoteOptionLen = original })
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
conf int
|
||||
want int
|
||||
}{
|
||||
{"配置小于存储上限时以配置为准", 100, 100},
|
||||
{"配置等于存储上限", db.MaxVoteOptionLen, db.MaxVoteOptionLen},
|
||||
{"配置超过存储上限时截到上限", db.MaxVoteOptionLen + 500, db.MaxVoteOptionLen},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
conf.Limits.VoteOptionLen = c.conf
|
||||
if got := voteOptionMaxLen(); got != c.want {
|
||||
t.Fatalf("voteOptionMaxLen() = %d, want %d", got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user