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 ./... 全通过。
226 lines
12 KiB
Go
226 lines
12 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 conf
|
||
|
||
// ⚠️ 仅应由 "-ldflags" 注入。
|
||
var (
|
||
BuildTime string
|
||
BuildCommit = "dev"
|
||
|
||
// StaticVersion 静态资源缓存版本号:base.css 与主题 css 共用,改样式只改这一处。
|
||
StaticVersion = "20261022c"
|
||
)
|
||
|
||
// 文本审核通道取值([security] text_censor_source)。定义在 conf 侧,
|
||
// 让配置校验(conf.Init)与通道装配(internal/security/censor)共用同一份字面量。
|
||
const (
|
||
CensorSourceAuto = "auto" // 七牛优先、阿里云兜底(历史行为,留空同义)
|
||
CensorSourceQiniu = "qiniu"
|
||
CensorSourceAliyun = "aliyun"
|
||
)
|
||
|
||
var (
|
||
App struct {
|
||
Production bool `ini:"production"`
|
||
ExternalURL string `ini:"external_url"`
|
||
ICP string `ini:"icp"`
|
||
DefaultLang string `ini:"default_lang"` // 站点默认语言(zh-CN/zh-TW/en/ja),留空回退简体中文
|
||
UptraceDSN string `ini:"uptrace_dsn"`
|
||
QiniuAccessKey string `ini:"qiniu_access_key"`
|
||
QiniuAccessSecret string `ini:"qiniu_access_secret"`
|
||
AliyunAccessKey string `ini:"aliyun_access_key"`
|
||
AliyunAccessKeySecret string `ini:"aliyun_access_key_secret"`
|
||
SentryDSN string `ini:"sentry_dsn"`
|
||
MaintenanceMode bool `ini:"maintenance_mode"`
|
||
ShareBackground string `ini:"share_background"`
|
||
FontPath string `ini:"font_path"`
|
||
}
|
||
|
||
// Theme 主题系统([theme] 段)。
|
||
// default :站点默认主题 slug,留空/非法回退内置 default(= 现在的外观)。
|
||
// available :启用哪些主题(逗号分隔),同时是箱主设置页的白名单;
|
||
// 列了但缺少 css 文件的会被忽略并告警(见 internal/theme)。
|
||
Theme struct {
|
||
Default string `ini:"default"`
|
||
Available string `ini:"available"`
|
||
}
|
||
|
||
// Announcement 站内公告([announcement] 段)。解析与展示判定见 announcement.go。
|
||
Announcement announcementConfig
|
||
|
||
Security struct {
|
||
EnableTextCensor bool `ini:"enable_text_censor"`
|
||
IPBlacklist string `ini:"ip_blacklist"`
|
||
IPBlacklistRedirectURL string `ini:"ip_blacklist_redirect_url"`
|
||
// TrustProxy 为 true 时信任 CDN 权威客户端头 CF-Connecting-IP / EO-Client-IP
|
||
// (以及受信代理设置的 X-Real-IP / X-Forwarded-For);否则一律用 RemoteAddr,
|
||
// 防止伪造 header 绕过速率限制与 IP 黑名单。
|
||
// ⚠️ 权威头只在 TCP 对端可信(回环,或命中 TrustedProxies)时才会被采信 ——
|
||
// 见 shared.ResolveClientIP:公网对端带来的权威头一律忽略(P1-3 修复)。
|
||
TrustProxy bool `ini:"trust_proxy"`
|
||
// TrustedProxies 逗号分隔的 IP / CIDR,仅限自己的反向代理 / LB。仅当 TCP 对端
|
||
// 落在该清单内才解析 X-Forwarded-For / X-Real-IP;留空则这两个头永不被信任。
|
||
// 回环地址(127.0.0.1 / ::1)无需登记即视为可信对端:cloudflared / 本机 nginx
|
||
// 回源的连接必然由本机进程发起,外部无法伪造。
|
||
// CDN 直连回源(非 Tunnel)的部署形态下,必须把该 CDN 的回源网段登记到这里,
|
||
// 否则它的权威头会被忽略、客户端 IP 退化为 CDN 边缘 IP。
|
||
TrustedProxies string `ini:"trusted_proxies"`
|
||
EnableAutoBan bool `ini:"enable_auto_ban"` // fail2ban 风格:检测到扫描特征后临时封禁来源 IP
|
||
// BanTrackNotFound 是否把未注册路径的 404 计入封禁统计。默认 false:仅命中后台
|
||
// 扫描特征路径(KindSensitive)才统计,避免共享出口(CDN 边缘 / CGNAT)下
|
||
// 正常用户被 404 探测流量连坐误伤。
|
||
BanTrackNotFound bool `ini:"ban_track_notfound"`
|
||
BanThresholdSensitive int `ini:"ban_threshold_sensitive"` // 窗口内敏感路径命中次数
|
||
BanThreshold404 int `ini:"ban_threshold_404"` // 窗口内 404 次数
|
||
BanWindowSeconds int `ini:"ban_window_seconds"` // 统计窗口(秒)
|
||
BanDurationMinutes int `ini:"ban_duration_minutes"` // 封禁时长(分钟)
|
||
// DefaultModerationStrictness 站点默认审核严格度(lax / standard / strict),
|
||
// 仅对未单独设置档位的账号生效(见 moderation.StrictnessFor)。留空或非法值回退
|
||
// strict(最严档),保证升级不会放宽拦截;默认拦截面可能较升级前略有扩大,
|
||
// 希望更少误伤则显式写 standard。
|
||
DefaultModerationStrictness string `ini:"default_moderation_strictness"`
|
||
// TextCensorSource 文本审核通道:qiniu / aliyun(内容安全 2.0 TextModeration,
|
||
// 个人实名账号即可开通)/ auto(默认,留空同义:七牛优先、阿里云复核)。
|
||
// 仅显式选定的通道会被调用,缺凭据时启动即报错(见 conf.Init)——
|
||
// 审核失败是 fail-closed(拒绝提交),会让发布与私信整站不可用且现象隐蔽。
|
||
TextCensorSource string `ini:"text_censor_source"`
|
||
// EnableHoneypot 开启「虚假后台登录界面」蜜罐:扫描器探测后台路径时
|
||
// 返回一个仿 WordPress 的假登录页,并记录其提交的用户名/密码等破解信息。
|
||
// 仅在「无任何路由匹配」的路径上生效,与真实业务路由零交集。
|
||
EnableHoneypot bool `ini:"enable_honeypot"`
|
||
// HoneypotPaths 追加自定义诱饵路径(逗号分隔),与内置清单并集。
|
||
HoneypotPaths string `ini:"honeypot_paths"`
|
||
// HoneypotRetainDays 蜜罐命中记录的保留天数,到期自动清理。
|
||
// ⚠️ 键名是 honeypot_retain_days(没有 tion)。写成 honeypot_retention_days
|
||
// 会被 ini 静默忽略 —— 现场踩过这个坑,表现为「自动清理未启用」。
|
||
// 缺省 30 天(见 sanitizeHoneypot);显式 <= 0 也回退 30。
|
||
// 要关闭自动清理请用 honeypot_auto_clean = false。
|
||
HoneypotRetainDays int `ini:"honeypot_retain_days"`
|
||
// HoneypotAutoClean 是否启用蜜罐记录的后台自动保留期清理。
|
||
// 缺省 true:honeypot_hits 是只追加的审计表,写入完全由外部扫描流量驱动,
|
||
// 没有自动清理的话表体积只增不减(这是「数据库被扫描撑爆」的真实路径)。
|
||
// 关闭后仍可手动执行 TamaBox honeypot prune。
|
||
// ⚠️ 缺省值靠 HasKey 判定(见 sanitizeHoneypot):ini 的 bool 解析是
|
||
// 「有键才赋值」,若只看零值就无法区分「未配置」与「显式关闭」。
|
||
HoneypotAutoClean bool `ini:"honeypot_auto_clean"`
|
||
// HoneypotMaxRows 蜜罐命中记录的行数上限。缺省 200000(约 53 MB)。
|
||
// 显式写 0 表示不设上限(逃生口,会告警提醒表只增不减)。
|
||
// 保留期是时间维度的上界,但攻击者可以用海量源 IP 在保留期内把表撑爆,
|
||
// 这是第二道闸。实测单行约 281 字节(含索引,常规 UA),20 万行约 53 MB。
|
||
HoneypotMaxRows int `ini:"honeypot_max_rows"`
|
||
// HoneypotThreshold 蜜罐「提交凭据」多少次即封禁。默认 1(一次即封):
|
||
// 往假后台里填账号密码是证据最强的行为,抓到一次就足够。
|
||
// ⚠️ 它不作用于纯路径探测(GET 看一眼假登录页)—— 那类请求走常规
|
||
// ban_threshold_sensitive,否则扫描器 GET 一次就被封,永远走不到
|
||
// POST 提交凭据那一步,蜜罐也就拿不到字典。
|
||
// 缺省回退 1(见 sanitizeHoneypot)。
|
||
HoneypotThreshold int `ini:"honeypot_threshold"`
|
||
// HoneypotBanMinutes 蜜罐封禁时长(分钟)。缺省 1440(24 小时),
|
||
// 刻意远长于 ban_duration_minutes:命中蜜罐意味着对方已在提交账号密码,
|
||
// 证据强度高于单纯路径探测(见 DefaultHoneypotBanMinutes)。
|
||
// 显式 <= 0 表示回退 ban_duration_minutes。
|
||
HoneypotBanMinutes int `ini:"honeypot_ban_minutes"`
|
||
// HoneypotCredDir 凭据取证文件的存放目录。
|
||
//
|
||
// ⚠️ 捕获到的用户名/密码**不入库**,只追加到这个目录下的按天文件
|
||
// (creds-YYYY-MM-DD.jsonl,一行一条 JSON)。原因:明文账号密码一旦入库,
|
||
// 就会跟着进备份、主从复制和任何能连库的报表工具,事后无法彻底清除。
|
||
// 目录会被强制为 0700、文件 0600;留空使用默认 data/honeypot-creds。
|
||
// 目录不可写时只降级为「不保存凭据」,不会影响站点启动。
|
||
HoneypotCredDir string `ini:"honeypot_cred_dir"`
|
||
// BanImagePath 被封禁时返回的图片路径(本地文件,相对路径以进程工作目录为基准)。
|
||
//
|
||
// 留空、文件读不到、或内容不是图片时,一律回退为纯文字 "403 Forbidden" ——
|
||
// 这条路径绝不能因为一张图把「拒绝」响应弄坏,更不能让缺失的资源变成 500。
|
||
//
|
||
// 只接受 image/* 的内容:如果配成 app.ini 之类的文件会被拒绝并告警,
|
||
// 否则等于把配置文件当响应体吐给每一个被封的攻击者。
|
||
BanImagePath string `ini:"ban_image_path"`
|
||
SecurityAlertEmail string `ini:"security_alert_email"` // 触发封禁时告警收件人(留空不发送)
|
||
|
||
// CSP 内容安全策略的生效模式:enforce / report / off,缺省 enforce。
|
||
//
|
||
// enforce 下发 Content-Security-Policy(真正拦截);report 只下发 Report-Only
|
||
// (照常加载、只记违规),用于临时观察。默认拦截而不是观察,原因是:
|
||
// 没人会盯着 console,且不给 csp_report_uri 时 Report-Only 等于什么都没做,
|
||
// 留个不生效的开关反而制造「已防护」的错觉。默认值的安全前提是白名单已经
|
||
// 用真浏览器逐页巡检过(csp_sweep.py:20 个页面零违规)。
|
||
// 上线后发现误伤,把 csp 改成 report 或 off 再重启即可恢复,不用改代码。
|
||
CSP string `ini:"csp"`
|
||
// CSPReportURI 违规上报地址(可选)。留空时浏览器只把违规打到 console 不上报;
|
||
// 用 report 模式观察时应当配上,否则「观察一周」就只是空谈。
|
||
CSPReportURI string `ini:"csp_report_uri"`
|
||
}
|
||
|
||
Server struct {
|
||
Port int `ini:"port"`
|
||
Listen string `ini:"listen"` // 绑定监听地址(含端口)。留空则默认 ":port"(监听所有网卡)。
|
||
Salt string `ini:"salt"`
|
||
XSRFKey string `ini:"xsrf_key"`
|
||
}
|
||
|
||
Database struct {
|
||
DSN string
|
||
|
||
Type string `ini:"type"`
|
||
User string `ini:"user"`
|
||
Password string `ini:"password"`
|
||
Host string `ini:"host"`
|
||
Port uint `ini:"port"`
|
||
Name string `ini:"name"`
|
||
Schema string `ini:"schema"`
|
||
DedupeUniqueFields bool `ini:"dedupe_unique_fields"`
|
||
}
|
||
|
||
Redis struct {
|
||
Addr string `ini:"addr"`
|
||
Password string `ini:"password"`
|
||
}
|
||
|
||
Recaptcha struct {
|
||
CaptchaType string `ini:"captcha_type"`
|
||
Domain string `ini:"domain"`
|
||
SiteKey string `ini:"site_key"`
|
||
ServerKey string `ini:"server_key"`
|
||
TurnstileStyle bool `ini:"turnstile_style"`
|
||
}
|
||
|
||
Pixel struct {
|
||
Host string `ini:"host"`
|
||
}
|
||
|
||
Upload struct {
|
||
DefaultAvatarURL string `ini:"default_avatar"`
|
||
DefaultBackground string `ini:"default_background"`
|
||
DefaultBackgroundImageURL string `ini:"default_qrbackgroundimage"`
|
||
AliyunEndpoint string `ini:"aliyun_endpoint"`
|
||
AliyunAccessID string `ini:"aliyun_access_id"`
|
||
AliyunAccessSecret string `ini:"aliyun_access_secret"`
|
||
AliyunBucket string `ini:"aliyun_bucket"`
|
||
AliyunBucketCDNHost string `ini:"aliyun_bucket_cdn_host"`
|
||
ImageEndpoint string `ini:"image_endpoint"`
|
||
ImageAccessID string `ini:"image_access_id"`
|
||
ImageAccessSecret string `ini:"image_access_secret"`
|
||
ImageBucket string `ini:"image_bucket"`
|
||
ImageBucketCDNHost string `ini:"image_bucket_cdn_host"`
|
||
}
|
||
|
||
Mail struct {
|
||
Account string `ini:"account"`
|
||
Password string `ini:"password"`
|
||
Port int `ini:"port"`
|
||
SMTP string `ini:"smtp"`
|
||
SkipTLSVerify bool `ini:"skip_tls_verify"` // 仅自签名 / 内网 SMTP 需要
|
||
}
|
||
|
||
Service struct {
|
||
Backends []struct {
|
||
Prefix string `ini:"prefix"`
|
||
ForwardURL string `ini:"forward_url"`
|
||
}
|
||
TLSInsecureSkipVerify bool `ini:"tls_insecure_skip_verify"` // 仅内网自签名后端需要
|
||
}
|
||
)
|