security: 会话可撤销与认证面加固

- users 加 token_version(schema v4):改密/登出递增,该用户全部已发 access/refresh 立即作废;refresh 改轮换式换发新 cookie
- 邀请码改精确匹配 + 注册成功后消费(一码一用),修复子串匹配让任意后缀放行、可无限薅体验金
- 生产校验拦下 .env.example 占位密钥;OT_ENV 枚举校验(拼错不再静默跳过断言);生产强制 COOKIE_SECURE;非 development 拒绝默认密码播种管理员
- WebAuthn 会话启用过期校验 + 4096 容量上限(未认证 login/begin 可无限撑爆内存),begin/complete 挂登录级限速
- /api/v1 挂 1MB 请求体上限;login 先限 IP 桶再解析 body;全局安全响应头 + CSP
- 连接池配置(PG 30/SQLite 8);SQLite DSN 自动追加 WAL + busy_timeout(驱动只认 _pragma,旧注释的 _journal_mode 写法不生效)
This commit is contained in:
Sakurasan committed 2026-10-29 02:56:46 +08:00
1 parent d6db0f8162
commit e3ff0d3329
14 files changed
+392 -37

No files matched your search

+21 -7
View File
@@ -241,26 +241,40 @@ func splitList(s string) []string {
return out
}
// validate 生产环境的密钥断言:这些默认值是公开已知的,忘配就等于没有认证。
//
// master key 缺失时 crypto.NewEncryptor 会用 sha256("") 派生密钥(结果同样公开),
// 渠道上游密钥的密文就变成谁都能解;jwt.secret 用默认值则能直接伪造管理员 token。
// Load 之后、使用之前统一校验。env 值枚举化:OT_ENV 拼错(如 "prod")会静默落入
// 开发配置,让下面所有生产断言形同虚设。
func (c *Config) validate() error {
if c.Env != "development" && c.Env != "production" {
return fmt.Errorf(`OT_ENV 只能是 "development" 或 "production",当前 %q(拼错会静默跳过生产密钥断言)`, c.Env)
}
if c.Env != "production" {
return nil
}
var problems []string
if c.JWT.Secret == "" || c.JWT.Secret == "dev-only-secret-change-me" {
if c.JWT.Secret == "" || knownSecret(c.JWT.Secret) {
problems = append(problems, "OT_JWT_SECRET 未设置或仍是仓库里的默认值(可伪造会话 token)")
}
if len(c.Master) < 16 {
problems = append(problems, "OT_MASTER_KEY 未设置或少于 16 字节(缺失时会用 sha256(\"\") 派生,渠道密文可被公开还原)")
if len(c.Master) < 16 || knownSecret(c.Master) {
problems = append(problems, "OT_MASTER_KEY 未设置、少于 16 字节或仍是示例占位值(缺失/公开时渠道密文可被公开还原)")
}
if c.AdminPassword == "" || c.AdminPassword == "admin123" {
problems = append(problems, "OT_ADMIN_PASSWORD 未设置或仍是 admin123")
}
if !c.JWT.CookieSecure {
problems = append(problems, "OT_JWT_COOKIE_SECURE 必须为 true(生产环境 refresh cookie 走 HTTP 明文可被中间人截取;自签证书或反代终止 TLS 后也要置 true)")
}
if len(problems) > 0 {
return fmt.Errorf("生产环境启动被拒绝:%s", strings.Join(problems, ";"))
}
return nil
}
// knownSecret 仓库/示例文件里公开出现过的密钥占位值:复制 .env.example 不改密钥
// 是最常见的部署路径,必须当作默认值拦下。
func knownSecret(s string) bool {
switch s {
case "dev-only-secret-change-me", "change-me-to-a-long-random-string", "change-me-master-key":
return true
}
return false
}
+11
View File
@@ -8,6 +8,7 @@ import (
func prodCfg() *Config {
c := &Config{Env: "production"}
c.JWT.Secret = "a-real-random-secret-not-the-default"
c.JWT.CookieSecure = true
c.Master = "0123456789abcdef0123456789abcdef"
c.AdminPassword = "strong-enough-password"
return c
@@ -25,10 +26,14 @@ func TestValidateProduction(t *testing.T) {
}{
{"默认 jwt secret", func(c *Config) { c.JWT.Secret = "dev-only-secret-change-me" }, "OT_JWT_SECRET"},
{"空 jwt secret", func(c *Config) { c.JWT.Secret = "" }, "OT_JWT_SECRET"},
// 复制 .env.example 不改密钥是最常见的部署路径,占位值必须拦下
{"示例文件里的 jwt secret 占位值", func(c *Config) { c.JWT.Secret = "change-me-to-a-long-random-string" }, "OT_JWT_SECRET"},
{"缺失 master key", func(c *Config) { c.Master = "" }, "OT_MASTER_KEY"},
{"过短 master key", func(c *Config) { c.Master = "short" }, "OT_MASTER_KEY"},
{"示例文件里的 master key 占位值", func(c *Config) { c.Master = "change-me-master-key" }, "OT_MASTER_KEY"},
{"默认管理员密码", func(c *Config) { c.AdminPassword = "admin123" }, "OT_ADMIN_PASSWORD"},
{"未设管理员密码", func(c *Config) { c.AdminPassword = "" }, "OT_ADMIN_PASSWORD"},
{"cookie 未开 secure", func(c *Config) { c.JWT.CookieSecure = false }, "OT_JWT_COOKIE_SECURE"},
}
for _, tc := range cases {
c := prodCfg()
@@ -55,11 +60,17 @@ func TestValidateProduction(t *testing.T) {
dev := prodCfg()
dev.Env = "development"
dev.JWT.Secret = "dev-only-secret-change-me"
dev.JWT.CookieSecure = false
dev.Master = ""
dev.AdminPassword = "admin123"
if err := dev.validate(); err != nil {
t.Fatalf("development 不应拦截:%v", err)
}
// env 拼错(如 "prod")会静默跳过全部生产断言,必须枚举校验直接拒绝
if err := (&Config{Env: "prod"}).validate(); err == nil || !strings.Contains(err.Error(), "OT_ENV") {
t.Fatalf("未知 env 值应被拒绝:%v", err)
}
}
func TestSplitList(t *testing.T) {