V6: 安全事故 —— 「有人猜到了管理员密码」
周一早上你打开系统,发现一条异常操作日志:实习生的账号访问了所有人的报销记录。排查下来发现三个问题同时存在:
- 管理员密码被猜到:admin 账号密码是
admin123,实习生试了三次就进去了 - 越权访问:改 URL 里的
user_id参数就能看到别人的报销数据,API 没有校验数据归属 - SQL 注入:搜索接口直接拼接用户输入,用
' OR 1=1--可以导出全部数据
老板的反应:“这要是被外面的人发现,公司数据全泄露了。”
当前状态 (V5):系统已有 JWT 鉴权(区分 user/admin 角色),但鉴权只验证”你是否登录”,没有验证”你能否访问这条数据”。密码存储和输入处理也有隐患。
问题分析(5 层框架)
Section titled “问题分析(5 层框架)”| 层级 | 问题 | 影响 |
|---|---|---|
| 表象 | 实习生看到了所有人的数据 | 数据泄露,信任危机 |
| 直接原因 | 弱密码 + 越权 + SQL 注入三重漏洞 | 攻击面极大 |
| 系统原因 | 认证 ≠ 授权,系统只做了认证 | 登录后无权限边界 |
| 设计缺失 | 没有纵深防御(defense in depth) | 一层被突破全线崩溃 |
| 根本原因 | 安全没有纳入开发流程 | 每次迭代都可能引入新漏洞 |
OWASP Top 10 命中情况
Section titled “OWASP Top 10 命中情况”- A01: Broken Access Control — 越权访问(改 user_id 看别人数据)
- A03: Injection — SQL 注入(搜索接口拼接字符串)
- A07: Identification and Authentication Failures — 弱密码、无登录频率限制
graph TD User["用户"] -->|"HTTPS"| Nginx["Nginx<br/>反向代理 + HTTPS"] Nginx --> Go["Go/Gin<br/>+ 限流<br/>+ 输入校验<br/>+ CORS"] Go --> Redis["Redis"] Go --> DB["PostgreSQL"]新增:HTTPS + Nginx + 限流 + 输入校验 解决:防止密码暴力破解、SQL 注入、XSS
| 决策点 | 选项 A | 选项 B | 选择 | 理由 |
|---|---|---|---|---|
| 密码策略 | 仅前端校验长度 | 后端强制策略 + bcrypt | B | 前端可绕过,必须后端兜底 |
| 授权模型 | 每个 handler 手动检查 | 中间件统一拦截 | B | 手动检查容易遗漏,中间件强制执行 |
| SQL 注入防护 | 手动 escape | GORM 参数化查询审查 | B | GORM 默认参数化,但要审查 Raw/手写 SQL |
| 登录限流 | 全局限流 | 按 IP + 账号双维度 | B | 防止分布式猜密码 + 单账号爆破 |
| 密钥管理 | 硬编码在代码里 | .env 文件 + 环境变量 | B | 代码会进 Git,密钥绝不能在代码里 |
安全加固(纵深防御):├── 第 1 层:密码策略(>= 8 位,含大小写 + 数字)+ bcrypt(cost=12)├── 第 2 层:登录限流(5 次失败锁定 15 分钟,Redis 计数)├── 第 3 层:授权中间件(从 JWT 取 user_id,强制过滤数据)├── 第 4 层:参数化查询审查(禁止字符串拼接 SQL)├── 第 5 层:安全响应头(CORS、CSP、X-Frame-Options 等)└── 第 6 层:敏感配置外置(.env,不进 Git)- 密码策略中间件:注册/改密码时后端校验强度,bcrypt 哈希存储
- 数据归属中间件:
AuthzMiddleware从 JWT 解析 user_id,注入到查询条件 - SQL 注入审查:全局搜索
db.Raw(和fmt.Sprintf,替换为参数化 - 登录限流:Redis 存
login_fail:{ip}和login_fail:{email},超过阈值返回 429 - CORS 配置:只允许前端域名,不用
* - 安全响应头中间件:统一设置 CSP、X-Content-Type-Options 等
给 AI 的 Prompt
Section titled “给 AI 的 Prompt”我有一个 Go(Gin) + GORM + PostgreSQL + Redis 的团队记账工具,发现了以下安全漏洞需要修复:1. 管理员密码是弱密码 admin1232. 修改 URL 中的 user_id 可以看到别人的报销数据3. 搜索接口存在 SQL 注入
请帮我做以下安全加固:
1. **密码策略 + bcrypt** - 新建 server/utils/password.go - ValidatePasswordStrength(pwd string) error:至少 8 位,包含大写、小写、数字 - HashPassword(pwd string) (string, error):bcrypt cost=12 - CheckPassword(hashed, pwd string) bool - 注册和修改密码接口调用校验,不符合返回 400 + 具体提示
2. **数据归属授权中间件** - 新建 server/middleware/authz.go - 从 Gin context 获取当前用户 ID(JWT 中间件已解析) - 普通用户:所有报销查询自动加 WHERE user_id = ? - 管理员:可查看所有数据 - 在 GET /api/expenses/:id 接口中,查询后校验数据归属,不匹配返回 403 - 在 PUT/DELETE 接口同样校验
3. **SQL 注入修复** - 审查所有用到 db.Raw()、db.Where() 传字符串拼接的地方 - 全部改为参数化查询:db.Where("field = ?", value) - 搜索接口示例: 错误:db.Where("name LIKE '%" + keyword + "%'") 正确:db.Where("name LIKE ?", "%"+keyword+"%")
4. **登录限流** - 新建 server/middleware/rate_limit.go - 使用 Redis INCR + EXPIRE 实现滑动窗口 - 规则:同一 IP 5 分钟内登录失败 10 次,锁定 15 分钟 - 同一邮箱 5 分钟内失败 5 次,锁定 15 分钟 - 锁定期间返回 HTTP 429 + Retry-After header - 登录成功后清除该 IP + 邮箱的失败计数
5. **CORS 配置** - 在 Gin 路由初始化时配置 CORS 中间件 - 允许的 Origin:从环境变量 ALLOWED_ORIGINS 读取(默认 http://localhost:5173) - 允许的方法:GET, POST, PUT, DELETE, OPTIONS - 允许的头:Content-Type, Authorization - 不要使用 AllowAllOrigins
6. **安全响应头中间件** - 新建 server/middleware/security_headers.go - X-Content-Type-Options: nosniff - X-Frame-Options: DENY - X-XSS-Protection: 0(现代浏览器用 CSP 替代) - Content-Security-Policy: default-src 'self' - Strict-Transport-Security: max-age=31536000; includeSubDomains
7. **环境变量管理** - 确保 JWT_SECRET 从 .env 读取,不在代码中硬编码 - .gitignore 中包含 .env - 提供 .env.example 模板(不含真实密钥)
请给出完整代码,每个文件标注路径。- 注册时输入
123456,返回 400 + “密码至少 8 位” - 注册时输入
abcdefgh,返回 400 + “密码需包含数字” - 数据库中密码字段是 bcrypt hash(以
$2a$开头),不是明文 - 原有用户登录不受影响(需做数据迁移或强制改密码)
- 普通用户 A 调用
GET /api/expenses/100(属于用户 B),返回 403 - 普通用户 A 调用
GET /api/expenses,只返回自己的数据 - 管理员调用同样接口,返回所有数据
- 普通用户尝试
DELETE /api/expenses/100(属于用户 B),返回 403
SQL 注入
Section titled “SQL 注入”- 搜索框输入
' OR 1=1--,不会返回所有数据 - 搜索框输入
'; DROP TABLE expenses;--,表不会被删除 -
grep -r "fmt.Sprintf.*WHERE" server/无结果(无拼接 SQL)
- 同一 IP 连续 10 次错误密码,第 11 次返回 429
- 等待 15 分钟后恢复(或用 Redis CLI 手动删除 key 测试)
- 登录成功后再故意输错,计数从 0 开始
-
curl -I http://localhost:8080/api/expenses包含所有安全头 - 浏览器控制台无 CORS 错误(正常请求)
- 从其他域名(如 localhost:3000)发请求,被 CORS 拒绝
你学到了什么
Section titled “你学到了什么”| 主题 | 对应模块 |
|---|---|
| OWASP Top 10 是 Web 安全的最低标准 | → Module 14 (安全/OWASP) |
| 认证(你是谁)和授权(你能做什么)是两件事 | → Module 12 (认证安全) |
| bcrypt 的 cost 参数平衡安全性和性能 | → Module 12 |
| 限流是防暴力破解的第一道防线 | → Module 8 (限流) |
| 纵深防御:任何单层防护都可能被突破 | → Module 14 |
| CORS 不是安全措施,是浏览器行为,API 层仍需授权 | → Module 14 |
1. bcrypt 比较返回 error 不是 bool
Section titled “1. bcrypt 比较返回 error 不是 bool”现象:密码明明正确,但登录失败。
原因:bcrypt.CompareHashAndPassword 返回 error,nil 代表匹配,非 nil 代表不匹配。不少人写成 if err != nil { 匹配 }。
解决:if err == nil { 密码正确 }。
2. 授权中间件遗漏了某些路由
Section titled “2. 授权中间件遗漏了某些路由”现象:大部分接口有权限控制,但有一个”导出 CSV”接口没有加中间件。
原因:新加的路由忘记挂载授权中间件。
解决:使用路由分组(router.Group),在组级别挂载中间件,新路由自动继承。
3. 限流 Key 设计不当
Section titled “3. 限流 Key 设计不当”现象:攻击者换 IP 后限流失效;或者正常用户共享出口 IP 被误伤。 原因:只按 IP 限流不够,只按账号限流也不够。 解决:IP + 账号双维度:IP 维度阈值高一些(10 次),账号维度阈值低一些(5 次)。
4. CORS 配置允许了 *
Section titled “4. CORS 配置允许了 *”现象:测试时图方便设了 AllowAllOrigins: true,上线忘了改。
原因:Access-Control-Allow-Origin: * 意味着任何网站都能调用你的 API。
解决:从环境变量读取允许的 Origin 列表,开发环境和生产环境分开配置。
5. SQL 注入修了接口没修 Raw 查询
Section titled “5. SQL 注入修了接口没修 Raw 查询”现象:修了 Where 里的拼接,但统计报表里的 db.Raw() 还在拼字符串。
原因:只搜了 Where,没搜 Raw、Exec、Order 等也能注入的地方。
解决:grep -rn "db.Raw\|db.Exec\|db.Order" server/ 全部审查。Order 不支持参数化,需白名单校验。