P1: - 外链图片:markdown 渲染层只放行站内与自有 CDN 图片。配置里的 image_bucket_cdn_host 常写成 host/path,统一归一化后再比对,避免把自家图一起剥掉 - 密码:页面/提问密码最小 6 位;密码比对挪到验证码之后;页面密码失败锁定 5 次/10 分钟 - 客户端 IP:CF-Connecting-IP / EO-Client-IP 只在 TCP 对端可信(回环或 trusted_proxies) 时采信,公网对端带来的权威头一律忽略 P2/P3: - 跳转参数收敛到 shared.SafeRedirectTarget:拒反斜杠与控制字符(url.Parse 会把 %0d%0a 解成 CRLF,伪造 Location),最后再用 path.Clean 验一次前缀 - 注册不再暴露「邮箱已注册 / 已停用」:与全新注册共用同一个成功出口,只在日志留痕 - CSP:默认 enforce,白名单由配置派生,覆盖 umami(运行时注入)、Turnstile 与 按 sentry_dsn 条件放行的 Sentry CDN - /user 与 /_ 下发 no-store;补 X-XSS-Protection、Permissions-Policy,并把安全头前移到 静态资源中间件之前(原先静态文件拿不到 nosniff/HSTS) - 封禁事实落库 ban_records;429 统一带 Retry-After 与本地化文案 - token 比较改常量时间;探索页页码上限;JSON 响应显式 Content-Type 全局复查补的两处: - 私聊 SendMessage 的 POST 缺 conv.OwnerID 校验,被拉黑者可换 domain 继续发消息 - 页面密码 next 与登录 to 同源的反斜杠外跳 验收:verify_p1_fixes 4/4、verify_round2 19/19、verify_page_next 6/6、 verify_chat_owner 3/3(含变异对照);单测与 go build ./... 全通过。
76 lines
4.1 KiB
Go
76 lines
4.1 KiB
Go
// Copyright 2022 E99p1ant. All rights reserved.
|
||
// Use of this source code is governed by a MIT-style
|
||
// license that can be found in the LICENSE file.
|
||
|
||
package form
|
||
|
||
type UpdateProfile struct {
|
||
Name string `valid:"required;maxlen:20" label:"昵称" msg:"form.profile.name"`
|
||
OldPassword string `label:"旧密码"`
|
||
NewPassword string `valid:"maxlen:30" label:"新密码" msg:"form.profile.passwordMax"`
|
||
Intro string `valid:"required;maxlen:100" label:"介绍" msg:"form.profile.intro"`
|
||
Usernamecolor string `valid:"required;minlen:7;maxlen:7" label:"用户名颜色" msg:"form.profile.colorHex"`
|
||
Introcolor string `valid:"required;minlen:7;maxlen:7" label:"个人说明颜色" msg:"form.profile.colorHex"`
|
||
Qrcodebackcolor string `valid:"required;minlen:7;maxlen:7" label:"二维码背景色" msg:"form.profile.colorHex"`
|
||
Qrcodecolor string `valid:"required;minlen:7;maxlen:7" label:"二维码前景色" msg:"form.profile.colorHex"`
|
||
Dotscale string `valid:"required" label:"二维码点大小" msg:"form.profile.dotscale"`
|
||
Backgroundimagealpha string `valid:"required" label:"二维码背景透明度" msg:"form.profile.backgroundimagealpha"`
|
||
Qrcodepdpcolor string `valid:"required;minlen:7;maxlen:7" label:"二维码探测图形颜色" msg:"form.profile.colorHex"`
|
||
NotifyEmail string `label:"开启邮箱通知"`
|
||
Acfunlink string `valid:"maxlen:30" label:"AcFun主页" msg:"form.profile.linkMax"`
|
||
Bililink string `valid:"maxlen:30" label:"Bili主页" msg:"form.profile.linkMax"`
|
||
Weibolink string `valid:"maxlen:30" label:"微博主页" msg:"form.profile.linkMax"`
|
||
Bloglink string `valid:"maxlen:30" label:"博客主页" msg:"form.profile.linkMax"`
|
||
Xiaohongshulink string `valid:"maxlen:30" label:"小红书主页" msg:"form.profile.linkMax"`
|
||
UseCustomQRColor string `form:"use_custom_qr_color"`
|
||
Language string `form:"language"`
|
||
}
|
||
|
||
// UpdateProfileTheme 主题外观。空串表示跟随站点默认主题;
|
||
// 取值在白名单之外时保存被拒绝(见 route/user.UpdateTheme),
|
||
// 这样箱主不会「选了个不存在的主题,然后看起来什么都没变」。
|
||
type UpdateProfileTheme struct {
|
||
Theme string `valid:"maxlen:32" label:"主题" msg:"profile.theme.invalid"`
|
||
}
|
||
|
||
// UpdateHarassment 仅「允许注册用户向我提问」开关(防骚扰类型)。
|
||
// 全局屏蔽词为独立入口 UpdateBlockWords:二者分处不同设置面板保存,
|
||
// 若仍共用一个结构体会互相覆盖对方字段(见 route/user/harassment.go 注释)。
|
||
type UpdateHarassment struct {
|
||
RegisterOnly string `label:"仅允许注册用户"`
|
||
}
|
||
|
||
type UpdateBlockWords struct {
|
||
BlockWords string `label:"屏蔽词"`
|
||
}
|
||
|
||
// UpdateModerationStrictness 内容审核严格度(lax / standard / strict)。
|
||
// 与屏蔽词一样是独立的设置入口:两者分处不同的表单,必须分开写,
|
||
// 否则单侧保存会把另一侧的值清空(见 route/user/harassment.go 顶部注释)。
|
||
type UpdateModerationStrictness struct {
|
||
Strictness string `form:"strictness"`
|
||
}
|
||
|
||
type UpdateTags struct {
|
||
Tags string `form:"tags"`
|
||
ShowInHome string `form:"show_in_home"`
|
||
}
|
||
|
||
// 页面访问密码 / 提问密码的最小长度。
|
||
//
|
||
// P1-2:这两个字段此前只有 maxlen,箱主可以设成 1 位或 1234 —— 而页面密码正是产品
|
||
// 对外宣传的访问控制,提问密码是箱主过滤骚扰的唯一门槛。6 位是最低可接受值
|
||
// (纯数字 10^6,配合失败锁定后穷举不可行);留空仍然合法,表示关闭该密码
|
||
// (govalid 对空值跳过 minlen / maxlen)。
|
||
const PasswordMinLen = 6
|
||
|
||
// 新增:页面访问密码表单
|
||
type UpdatePagePassword struct {
|
||
Password string `valid:"minlen:6;maxlen:30" label:"页面访问密码" msg:"form.profile.passwordLen"` // 留空表示关闭
|
||
}
|
||
|
||
// 新增:提问密码表单
|
||
type UpdateAskPassword struct {
|
||
Password string `valid:"minlen:6;maxlen:30" label:"提问密码" msg:"form.profile.passwordLen"` // 留空表示关闭
|
||
}
|