跳转到内容

V6: 安全事故 —— 「有人猜到了管理员密码」

周一早上你打开系统,发现一条异常操作日志:实习生的账号访问了所有人的报销记录。排查下来发现三个问题同时存在:

  1. 管理员密码被猜到:admin 账号密码是 admin123,实习生试了三次就进去了
  2. 越权访问:改 URL 里的 user_id 参数就能看到别人的报销数据,API 没有校验数据归属
  3. SQL 注入:搜索接口直接拼接用户输入,用 ' OR 1=1-- 可以导出全部数据

老板的反应:“这要是被外面的人发现,公司数据全泄露了。”

当前状态 (V5):系统已有 JWT 鉴权(区分 user/admin 角色),但鉴权只验证”你是否登录”,没有验证”你能否访问这条数据”。密码存储和输入处理也有隐患。


层级问题影响
表象实习生看到了所有人的数据数据泄露,信任危机
直接原因弱密码 + 越权 + SQL 注入三重漏洞攻击面极大
系统原因认证 ≠ 授权,系统只做了认证登录后无权限边界
设计缺失没有纵深防御(defense in depth)一层被突破全线崩溃
根本原因安全没有纳入开发流程每次迭代都可能引入新漏洞
  • 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选择理由
密码策略仅前端校验长度后端强制策略 + bcryptB前端可绕过,必须后端兜底
授权模型每个 handler 手动检查中间件统一拦截B手动检查容易遗漏,中间件强制执行
SQL 注入防护手动 escapeGORM 参数化查询审查BGORM 默认参数化,但要审查 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)
  1. 密码策略中间件:注册/改密码时后端校验强度,bcrypt 哈希存储
  2. 数据归属中间件AuthzMiddleware 从 JWT 解析 user_id,注入到查询条件
  3. SQL 注入审查:全局搜索 db.Raw(fmt.Sprintf,替换为参数化
  4. 登录限流:Redis 存 login_fail:{ip}login_fail:{email},超过阈值返回 429
  5. CORS 配置:只允许前端域名,不用 *
  6. 安全响应头中间件:统一设置 CSP、X-Content-Type-Options 等

我有一个 Go(Gin) + GORM + PostgreSQL + Redis 的团队记账工具,发现了以下安全漏洞需要修复:
1. 管理员密码是弱密码 admin123
2. 修改 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
  • 搜索框输入 ' 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 拒绝

主题对应模块
OWASP Top 10 是 Web 安全的最低标准→ Module 14 (安全/OWASP)
认证(你是谁)和授权(你能做什么)是两件事→ Module 12 (认证安全)
bcrypt 的 cost 参数平衡安全性和性能→ Module 12
限流是防暴力破解的第一道防线→ Module 8 (限流)
纵深防御:任何单层防护都可能被突破→ Module 14
CORS 不是安全措施,是浏览器行为,API 层仍需授权→ Module 14

现象:密码明明正确,但登录失败。 原因bcrypt.CompareHashAndPassword 返回 error,nil 代表匹配,非 nil 代表不匹配。不少人写成 if err != nil { 匹配 }解决if err == nil { 密码正确 }

现象:大部分接口有权限控制,但有一个”导出 CSV”接口没有加中间件。 原因:新加的路由忘记挂载授权中间件。 解决:使用路由分组(router.Group),在组级别挂载中间件,新路由自动继承。

现象:攻击者换 IP 后限流失效;或者正常用户共享出口 IP 被误伤。 原因:只按 IP 限流不够,只按账号限流也不够。 解决:IP + 账号双维度:IP 维度阈值高一些(10 次),账号维度阈值低一些(5 次)。

现象:测试时图方便设了 AllowAllOrigins: true,上线忘了改。 原因Access-Control-Allow-Origin: * 意味着任何网站都能调用你的 API。 解决:从环境变量读取允许的 Origin 列表,开发环境和生产环境分开配置。

5. SQL 注入修了接口没修 Raw 查询

Section titled “5. SQL 注入修了接口没修 Raw 查询”

现象:修了 Where 里的拼接,但统计报表里的 db.Raw() 还在拼字符串。 原因:只搜了 Where,没搜 RawExecOrder 等也能注入的地方。 解决grep -rn "db.Raw\|db.Exec\|db.Order" server/ 全部审查。Order 不支持参数化,需白名单校验。