fix(conf): 样例配置去掉行尾注释(会被并入值导致静默失效)
conf.Init 用 ini.LoadOptions{IgnoreInlineComment: true},该选项的语义是
**不剥离**行尾注释:`port = 1440 ; 注释` 解析出来的值是 "1440 ; 注释",
int 解析失败,而 MapTo 仍然返回 nil error —— 配置项静默回退零值。
实测照抄 app.sample.ini 的后果:
- text_censor_source 的值带注释,被 validateTextCensorSource 拒绝,直接起不来;
- ip_blacklist / security_alert_email 变成带注释的垃圾字符串;
- honeypot_max_rows / honeypot_auto_clean 静默失效(后者靠 sanitize 兜底才没出事)。
改动:
- 17 处 `key = value ; 注释` 改成「注释独占一行 + 裸值」,保留 CRLF;
- 蜜罐段补 honeypot_cred_dir,以及「提交凭据才封禁」的语义说明;
- 新增 TestSampleIniValuesHaveNoInlineComment:遍历所有段的键,断言值不含 ';'
(比逐字段断言彻底,新增配置项自动纳入)。
变异验证:给 ban_window_seconds 加回行尾注释,测试按预期失败;
- keynorm_test 补 HoneypotBanMinutes 的缺省值断言(缺省 1440 / 显式 0 交给 autoban 回退)。
This commit is contained in:
+89
-44
@@ -6,22 +6,27 @@
|
||||
|
||||
[app]
|
||||
production = true
|
||||
external_url = "https://box.shiroko.one" ; 【必填】站点对外访问地址。用于生成 og:url 与邮件中的全部链接
|
||||
; 【必填】站点对外访问地址。用于生成 og:url 与邮件中的全部链接
|
||||
external_url = "https://box.shiroko.one"
|
||||
; (找回密码、私信通知等),必须以 http(s):// 开头、结尾不带斜杠
|
||||
; (程序会自动 TrimRight "/")。域名迁移时只改这一处即可全局生效。
|
||||
icp = ""
|
||||
default_lang = "" ; 站点默认语言:zh-CN / zh-TW / en / ja,留空默认简体中文(访客未设置语言时的兜底)
|
||||
; 站点默认语言:zh-CN / zh-TW / en / ja,留空默认简体中文(访客未设置语言时的兜底)
|
||||
default_lang = ""
|
||||
uptrace_dsn = ""
|
||||
qiniu_access_key = ""
|
||||
qiniu_access_secret = ""
|
||||
aliyun_access_key = ""
|
||||
aliyun_access_key_secret = ""
|
||||
share_background = "https://your-default-background.jpg" ; 默认分享图背景图链接(1280x720)
|
||||
font_path = "static/fonts/NotoSansSC-Regular.otf" ; 中文字体文件路径(相对项目根目录)
|
||||
; 默认分享图背景图链接(1280x720)
|
||||
share_background = "https://your-default-background.jpg"
|
||||
; 中文字体文件路径(相对项目根目录)
|
||||
font_path = "static/fonts/NotoSansSC-Regular.otf"
|
||||
|
||||
[security]
|
||||
enable_text_censor = true
|
||||
text_censor_source = auto ; 文本审核服务通道:qiniu(七牛)/ aliyun(阿里云)/ auto(七牛优先、阿里云兜底)。
|
||||
; 文本审核服务通道:qiniu(七牛)/ aliyun(阿里云)/ auto(七牛优先、阿里云兜底)。
|
||||
text_censor_source = auto
|
||||
; 留空等同于 auto,即引入该配置前的历史行为。
|
||||
; 显式选 qiniu / aliyun 时必须在 [app] 配好对应凭据,否则启动即报错
|
||||
; —— 避免运行期审核必然失败(审核失败是 fail-closed,拒绝提交,
|
||||
@@ -40,9 +45,12 @@ text_censor_source = auto ; 文本审核服务通道:qiniu(七牛
|
||||
; 调用会永久返回 code=596 "You have not opened Yundun Content Moderation Service"。
|
||||
; 审核失败时日志会打印阿里云返回的 code / msg / requestId 便于定位;
|
||||
; 首次出现账号级失败(未开通 / 未授权 / 密钥错误)还会额外打印一条一次性告警。
|
||||
ip_blacklist = ; 逗号分隔的IP黑名单,支持精确IP(如1.2.3.4)和CIDR(如1.2.3.0/24),留空不启用
|
||||
ip_blacklist_redirect_url = ; 黑名单IP访问时强制跳转到该URL
|
||||
trust_proxy = false ; 是否信任反向代理设置的 CF-Connecting-IP / X-Real-IP / X-Forwarded-For 头。
|
||||
; 逗号分隔的IP黑名单,支持精确IP(如1.2.3.4)和CIDR(如1.2.3.0/24),留空不启用
|
||||
ip_blacklist =
|
||||
; 黑名单IP访问时强制跳转到该URL
|
||||
ip_blacklist_redirect_url =
|
||||
; 是否信任反向代理设置的 CF-Connecting-IP / X-Real-IP / X-Forwarded-For 头。
|
||||
trust_proxy = false
|
||||
; 仅当站点部署在可信反向代理(如 nginx/Cloudflare)之后且代理已覆盖这些头时设为 true;
|
||||
; 否则保持 false,防止客户端伪造 header 绕过速率限制与 IP 黑名单。
|
||||
; ⚠️ 若站点在 Cloudflare 后且需使用下面的自动封禁,必须设为 true,
|
||||
@@ -65,47 +73,83 @@ trusted_proxies =
|
||||
; 留空(默认)则这两个头永不被信任,仅采用 CDN 权威头 CF-Connecting-IP /
|
||||
; EO-Client-IP(需源站已锁定)。
|
||||
; 注意:此清单用于你自己的基础设施,不是 CDN 的全量 CIDR 清单。
|
||||
enable_auto_ban = true ; 自动封禁(fail2ban 风格):检测到批量扫描/探测后临时封禁来源 IP
|
||||
ban_track_notfound = false ; 是否把「未注册路径的 404」计入封禁统计(默认 false)。正常业务
|
||||
; 自动封禁(fail2ban 风格):检测到批量扫描/探测后临时封禁来源 IP
|
||||
enable_auto_ban = true
|
||||
; 是否把「未注册路径的 404」计入封禁统计(默认 false)。正常业务
|
||||
ban_track_notfound = false
|
||||
; (域名/题目不存在等均走 302 业务跳转,不产生 404 计数)一律放行,
|
||||
; 仅命中后台扫描特征路径(/admin、/.env 等)才参与统计封禁,避免
|
||||
; 共享出口(CDN 边缘 / CGNAT)下正常用户被 404 探测连坐误伤。
|
||||
; 如需恢复对纯 404 字典扫描的封禁再置 true。
|
||||
ban_threshold_sensitive = 10 ; 统计窗口内命中敏感路径(/admin、/.env、/wp-admin 等)的次数阈值
|
||||
ban_threshold_404 = 20 ; 统计窗口内返回 404 的次数阈值(仅 ban_track_notfound = true 时生效)
|
||||
ban_window_seconds = 60 ; 统计窗口时长(秒)
|
||||
ban_duration_minutes = 15 ; 临时封禁时长(分钟),到期自动解除,重启即清零
|
||||
security_alert_email = ; 触发封禁时发送告警邮件的收件人(复用下方 SMTP 发信配置),留空不发送
|
||||
; 统计窗口内命中敏感路径(/admin、/.env、/wp-admin 等)的次数阈值
|
||||
ban_threshold_sensitive = 10
|
||||
; 统计窗口内返回 404 的次数阈值(仅 ban_track_notfound = true 时生效)
|
||||
ban_threshold_404 = 20
|
||||
; 统计窗口时长(秒)
|
||||
ban_window_seconds = 60
|
||||
; 临时封禁时长(分钟),到期自动解除,重启即清零
|
||||
ban_duration_minutes = 15
|
||||
; 触发封禁时发送告警邮件的收件人(复用下方 SMTP 发信配置),留空不发送
|
||||
security_alert_email =
|
||||
|
||||
; ── 蜜罐(虚假后台登录界面)────────────────────────────────────────────
|
||||
enable_honeypot = false ; 开启后,扫描器探测后台路径(/wp-admin、/.ssh、/.env 等)
|
||||
; 时不再返回 302/404,而是返回一个仿 WordPress 的假登录页,
|
||||
; 并把其提交的用户名/密码记录入库供取证。
|
||||
; 只在「没有任何路由匹配」的路径上生效,真实业务路由零交集;
|
||||
; 假页面只含 canary 标记与假凭据,绝不含任何真实信息。
|
||||
honeypot_paths = ; 追加自定义诱饵路径(逗号分隔),与内置清单取并集。
|
||||
honeypot_retain_days = 30 ; 蜜罐命中记录的保留天数(取证用途),到期自动清理。
|
||||
; ⚠️ 键名是 honeypot_retain_days(没有 tion)。写错会被静默忽略,
|
||||
; 程序会尝试自动纠正并在日志里提示,但别指望它每次都猜对。
|
||||
; 缺省 30 天;写 0 或负数会回退 30(不表示「永不清理」)。
|
||||
; 要关闭自动清理请用 honeypot_auto_clean = false。
|
||||
; 可用 TamaBox honeypot size 查看行数与磁盘占用。
|
||||
honeypot_auto_clean = true ; 是否启用蜜罐记录的后台自动保留期清理(每小时一轮)。
|
||||
; honeypot_hits 是只追加的审计表,写入完全由外部扫描流量
|
||||
; 驱动,不清理的话表体积只增不减。关闭后仍可手动 prune。
|
||||
; 缺省为 true(开启)。
|
||||
honeypot_max_rows = 200000 ; 蜜罐命中记录的行数上限(第二道闸)。缺省 200000。
|
||||
; 显式写 0 = 不限(会告警提醒表只增不减)。
|
||||
; 保留期是时间维度上界,但攻击者可用海量源 IP 在保留期内
|
||||
; 把表撑爆。实测单行约 281 字节(含索引,常规 UA),20 万行约 53 MB。
|
||||
; 实测数据:连续删 12000 插 12000,行数恒定 5 万,文件稳定在 15 MB 不再增长
|
||||
; (VACUUM 让空间可复用,不保证文件缩小)。
|
||||
honeypot_threshold = 1 ; 窗口内蜜罐命中多少次即封禁。默认 1(一次即封):蜜罐路径
|
||||
; 与真实业务路由零交集,正常访客不可能命中,一次即足以判定扫描器。
|
||||
honeypot_ban_minutes = ; 蜜罐封禁时长(分钟)。留空则复用 ban_duration_minutes(15)。
|
||||
; ⚠️ 蜜罐封禁与 enable_auto_ban 相互独立:即使关掉自动封禁,
|
||||
; 只要 enable_honeypot = true,蜜罐命中仍会封禁并记录。
|
||||
default_moderation_strictness = ; 站点默认「内容审核严格度」:lax(宽松)/ standard(标准)/ strict(严格)。
|
||||
; ⚠️ 本段一律「注释独占一行、值单独一行」。
|
||||
; 配置加载器开启了 IgnoreInlineComment,行尾 `; 注释` 会**并入值**:
|
||||
; honeypot_max_rows = 200000 ; 说明 → 值变成 "200000 ; 说明" → int 解析失败
|
||||
; honeypot_paths = ; 说明 → 值变成 " ; 说明" → 被切成一堆假路径
|
||||
; 后果是该项静默回退零值,而日志里只会说「配置值不对」,很难联想到是注释造成的。
|
||||
; 本文件其余段落已全部改成这个写法,新增配置项时请保持一致。
|
||||
|
||||
; 开启后,扫描器探测后台路径(/wp-admin、/.ssh、/.env 等)时不再返回 302,
|
||||
; 而是返回一个仿 WordPress 的假登录页,并把其提交的用户名/密码写入取证文件。
|
||||
; 只在「没有任何路由匹配」的路径上生效,真实业务路由零交集。
|
||||
; 假页面只含 canary 标记与假表单,绝不含任何真实信息。
|
||||
enable_honeypot = false
|
||||
|
||||
; 追加自定义诱饵路径(逗号分隔),与内置清单取并集。留空 = 只用内置清单。
|
||||
; 用 `TamaBox honeypot paths` 可查看最终生效的完整清单。
|
||||
honeypot_paths =
|
||||
|
||||
; 蜜罐命中记录的保留天数(取证用途),到期自动清理。
|
||||
; ⚠️ 键名是 honeypot_retain_days(没有 tion)。写错会被静默忽略,
|
||||
; 程序会尝试自动纠正并在日志里提示,但别指望它每次都猜对。
|
||||
; 缺省 30 天;写 0 或负数会回退 30(不表示「永不清理」)。
|
||||
; 要关闭自动清理请用 honeypot_auto_clean = false。
|
||||
honeypot_retain_days = 30
|
||||
|
||||
; 是否启用蜜罐记录的后台自动保留期清理(每小时一轮)。
|
||||
; honeypot_hits 是只追加的审计表,写入完全由外部扫描流量驱动,
|
||||
; 不清理的话表体积只增不减。关闭后仍可手动 `TamaBox honeypot prune`。
|
||||
honeypot_auto_clean = true
|
||||
|
||||
; 蜜罐命中记录的行数上限(第二道闸)。显式写 0 = 不限(会告警提醒表只增不减)。
|
||||
; 保留期是时间维度上界,但攻击者可用海量源 IP 在保留期内把表撑爆。
|
||||
; 实测单行约 281 字节(含索引,常规 UA),20 万行约 53 MB。
|
||||
honeypot_max_rows = 200000
|
||||
|
||||
; 「提交凭据」多少次即封禁。默认 1(一次即封):往假后台里填账号密码是证据最强
|
||||
; 的行为,抓到一次就足够。⚠️ 不作用于纯 GET 探测 —— 那类请求走 ban_threshold_sensitive,
|
||||
; 否则扫描器看一眼假登录页就被封,永远走不到提交凭据那一步。
|
||||
honeypot_threshold = 1
|
||||
|
||||
; 蜜罐封禁时长(分钟)。缺省 1440(24 小时),刻意远长于 ban_duration_minutes:
|
||||
; 命中蜜罐意味着对方已在提交账号密码,证据强度远高于单纯路径探测。
|
||||
; 显式写 0 = 回退 ban_duration_minutes。
|
||||
; ⚠️ 蜜罐封禁与 enable_auto_ban 相互独立:即使关掉自动封禁,
|
||||
; 只要 enable_honeypot = true,蜜罐命中仍会封禁并记录。
|
||||
honeypot_ban_minutes = 1440
|
||||
|
||||
; 凭据取证文件的存放目录。
|
||||
; ⚠️ 捕获到的用户名/密码**不入库**,只追加到这个目录下的按天文件
|
||||
; (creds-YYYY-MM-DD.jsonl,一行一条 JSON)。原因:明文账号密码一旦入库,
|
||||
; 就会跟着进备份、主从复制和任何能连库的报表工具,事后无法彻底清除。
|
||||
; 目录会被强制为 0700、文件 0600;相对路径以进程工作目录为基准。
|
||||
; 目录不可写时只降级为「不保存凭据」,不影响站点启动。
|
||||
; 建议单独挂一个卷,并让它不参与备份。
|
||||
honeypot_cred_dir = data/honeypot-creds
|
||||
|
||||
; 站点默认「内容审核严格度」:lax(宽松)/ standard(标准)/ strict(严格)。
|
||||
default_moderation_strictness =
|
||||
; 仅对未单独设置档位的账号生效(账号在设置页选择后以账号设置为准)。
|
||||
; 留空回退 strict(最严档):保证升级不会放宽任何拦截。
|
||||
; 严格档是三个档位中最严的(拦截面不小于升级前),因此部署后默认拦截面可能略有扩大;
|
||||
@@ -139,7 +183,8 @@ addr = "127.0.0.1:6379"
|
||||
password = ""
|
||||
|
||||
[recaptcha]
|
||||
captcha_type = recaptcha ; recaptcha(默认), turnstile, math(本地算数验证码,国内访问友好)
|
||||
; recaptcha(默认), turnstile, math(本地算数验证码,国内访问友好)
|
||||
captcha_type = recaptcha
|
||||
domain = "https://www.recaptcha.net"
|
||||
site_key = ""
|
||||
server_key = ""
|
||||
|
||||
@@ -236,6 +236,7 @@ func TestSanitizeHoneypotDefaults(t *testing.T) {
|
||||
wantMaxRows int
|
||||
wantAutoClean bool
|
||||
wantThreshold int
|
||||
wantBan int
|
||||
}{
|
||||
{
|
||||
name: "全未配置时回退安全默认值",
|
||||
@@ -244,6 +245,7 @@ func TestSanitizeHoneypotDefaults(t *testing.T) {
|
||||
wantMaxRows: DefaultHoneypotMaxRows,
|
||||
wantAutoClean: true,
|
||||
wantThreshold: DefaultHoneypotThreshold,
|
||||
wantBan: DefaultHoneypotBanMinutes,
|
||||
},
|
||||
{
|
||||
name: "显式关闭自动清理被尊重",
|
||||
@@ -252,6 +254,7 @@ func TestSanitizeHoneypotDefaults(t *testing.T) {
|
||||
wantMaxRows: DefaultHoneypotMaxRows,
|
||||
wantAutoClean: false,
|
||||
wantThreshold: DefaultHoneypotThreshold,
|
||||
wantBan: DefaultHoneypotBanMinutes,
|
||||
},
|
||||
{
|
||||
name: "显式保留期被采用",
|
||||
@@ -260,6 +263,7 @@ func TestSanitizeHoneypotDefaults(t *testing.T) {
|
||||
wantMaxRows: DefaultHoneypotMaxRows,
|
||||
wantAutoClean: true,
|
||||
wantThreshold: DefaultHoneypotThreshold,
|
||||
wantBan: DefaultHoneypotBanMinutes,
|
||||
},
|
||||
{
|
||||
name: "保留期写成 0 回退默认而非永不清理",
|
||||
@@ -268,6 +272,7 @@ func TestSanitizeHoneypotDefaults(t *testing.T) {
|
||||
wantMaxRows: DefaultHoneypotMaxRows,
|
||||
wantAutoClean: true,
|
||||
wantThreshold: DefaultHoneypotThreshold,
|
||||
wantBan: DefaultHoneypotBanMinutes,
|
||||
},
|
||||
{
|
||||
name: "行数上限显式 0 表示不限",
|
||||
@@ -276,6 +281,27 @@ func TestSanitizeHoneypotDefaults(t *testing.T) {
|
||||
wantMaxRows: 0,
|
||||
wantAutoClean: true,
|
||||
wantThreshold: DefaultHoneypotThreshold,
|
||||
wantBan: DefaultHoneypotBanMinutes,
|
||||
},
|
||||
{
|
||||
name: "显式封禁时长被采用",
|
||||
src: "[security]\nhoneypot_ban_minutes = 90\n",
|
||||
wantRetain: DefaultHoneypotRetainDays,
|
||||
wantMaxRows: DefaultHoneypotMaxRows,
|
||||
wantAutoClean: true,
|
||||
wantThreshold: DefaultHoneypotThreshold,
|
||||
wantBan: 90,
|
||||
},
|
||||
{
|
||||
// 0 是逃生口:留着让 autoban 回退 ban_duration_minutes,
|
||||
// sanitize 不做覆盖(只告警),故期望值仍是 0。
|
||||
name: "封禁时长写 0 交给 autoban 回退 ban_duration_minutes",
|
||||
src: "[security]\nhoneypot_ban_minutes = 0\n",
|
||||
wantRetain: DefaultHoneypotRetainDays,
|
||||
wantMaxRows: DefaultHoneypotMaxRows,
|
||||
wantAutoClean: true,
|
||||
wantThreshold: DefaultHoneypotThreshold,
|
||||
wantBan: 0,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -290,6 +316,7 @@ func TestSanitizeHoneypotDefaults(t *testing.T) {
|
||||
Security.HoneypotMaxRows = 0
|
||||
Security.HoneypotAutoClean = false
|
||||
Security.HoneypotThreshold = 0
|
||||
Security.HoneypotBanMinutes = 0
|
||||
File = f
|
||||
if err := f.Section("security").MapTo(&Security); err != nil {
|
||||
t.Fatalf("map security: %v", err)
|
||||
@@ -308,6 +335,9 @@ func TestSanitizeHoneypotDefaults(t *testing.T) {
|
||||
if Security.HoneypotThreshold != c.wantThreshold {
|
||||
t.Errorf("HoneypotThreshold = %d, want %d", Security.HoneypotThreshold, c.wantThreshold)
|
||||
}
|
||||
if Security.HoneypotBanMinutes != c.wantBan {
|
||||
t.Errorf("HoneypotBanMinutes = %d, want %d", Security.HoneypotBanMinutes, c.wantBan)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -358,6 +388,34 @@ func TestSampleIniHasNoUnknownKeys(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSampleIniValuesHaveNoInlineComment 守住「行尾注释会被并入值」的陷阱。
|
||||
//
|
||||
// conf.Init 用 ini.LoadOptions{IgnoreInlineComment: true},该选项的语义是
|
||||
// **不剥离**行尾注释(实测 `port = 1440 ; 注释` → 值 "1440 ; 注释",
|
||||
// int 解析失败,而 MapTo 仍然返回 nil error)。后果是该配置项静默回退零值,
|
||||
// 日志里只会说「配置值不对」,根本联想不到是注释造成的。
|
||||
//
|
||||
// 样例配置曾经全线是 `key = value ; 注释` 的写法:text_censor_source 因此
|
||||
// 让 conf.Init 直接拒绝启动,ip_blacklist / security_alert_email 变成带注释的
|
||||
// 垃圾字符串,honeypot_max_rows / honeypot_auto_clean 静默失效。
|
||||
//
|
||||
// 直接检查每个键的值里没有 ';',比逐个字段断言更彻底,也不会漏掉新加的配置项。
|
||||
func TestSampleIniValuesHaveNoInlineComment(t *testing.T) {
|
||||
path := filepath.Join("..", "..", "conf", "app.sample.ini")
|
||||
f, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, path)
|
||||
if err != nil {
|
||||
t.Skipf("跳过:无法读取样例配置 %s(%v)", path, err)
|
||||
}
|
||||
for _, sec := range f.Sections() {
|
||||
for _, k := range sec.Keys() {
|
||||
if strings.Contains(k.Value(), ";") {
|
||||
t.Errorf("样例配置 [%s] %s 的值含 ';':%q —— 行尾注释会被并入值,该项将静默失效",
|
||||
sec.Name(), k.Name(), k.Value())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeAllSectionKeysEndToEnd 走完整链路:
|
||||
// 加载带错拼键的配置 → 归一化 → MapTo → 拿到正确的保留期。
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user