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:
1 parent
d6db0f8162
commit
e3ff0d3329
14 files changed
+392
-37
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user