1. AI 修 Bug 的蜜月期,为什么总在安全上翻车
用 Cursor、Claude Code、Trae 这类 AI 编程工具修 Bug,效率提升是肉眼可见的。以前一个空指针异常要翻半小时日志,现在把报错贴进去,几秒钟就给你定位到具体行,连修复代码都写好了。我身边不少朋友已经把 AI 当成了日常 Debug 的第一入口,遇到问题先问 AI,实在不行再自己动手。
但用得越多,我越发现一个规律:AI 修 Bug 的能力和它引入安全漏洞的能力,几乎是同一套机制在起作用。它太擅长"让代码跑起来"了,以至于经常忽略"让代码安全地跑起来"。这两件事在 AI 眼里优先级完全不同——前者是显性目标,后者是隐性约束。
举个最常见的场景。你让 AI 修一个登录接口报 500 的错误,它检查后发现是数据库查询语句拼接有问题,于是顺手帮你把 SQL 改成了字符串拼接的形式,错误确实没了,接口通了。但你仔细一看,它把用户输入直接拼进了 SQL 语句里。这就是典型的 SQL 注入入口,而 AI 在修复过程中完全没有提示你这一点。
我踩过的坑不止这一个。有一次让 AI 帮我修一个文件上传的 Bug,它为了"简化逻辑",把文件类型校验直接删掉了,理由是"这个校验导致了误判"。功能是正常了,但上传接口变成了任意文件上传。还有一次修权限相关的 Bug,AI 把中间件的鉴权逻辑注释掉来"排除干扰",修完之后忘了恢复。
这些问题的共同点是:AI 的修复目标是"消除报错",而不是"消除报错且不引入新风险"。它不会主动告诉你"我这样改会带来什么安全隐患",因为它的训练目标里,功能正确性的权重远高于安全性。
所以这篇文章想聊的不是"AI 修 Bug 好不好用",而是在用 AI 修 Bug 的过程中,哪几类安全问题最容易翻车,以及怎么建立一套自己的检查习惯。适合所有把 AI 编程工具纳入日常工作流的开发者,不管你是刚入门的新手,还是已经用了大半年的老用户,这几个坑大概率你都遇到过,只是没意识到。
2. 第一类翻车:SQL 拼接被"顺手"改回来
2.1 为什么 AI 特别容易在这里翻车
SQL 注入是安全领域最经典的漏洞之一,按理说 AI 应该很熟悉。但实际情况是,AI 在修 Bug 时经常主动把参数化查询改回字符串拼接,而且改得理直气壮。
原因在于 AI 的修复逻辑是"最小改动原则"。假设原始代码是这样的:
def get_user(username): sql = "SELECT * FROM users WHERE username = %s" cursor.execute(sql, (username,)) return cursor.fetchone()如果这个函数报错,比如因为参数类型不匹配导致查询失败,AI 的第一反应往往是"参数化查询的写法有问题",然后改成:
def get_user(username): sql = f"SELECT * FROM users WHERE username = '{username}'" cursor.execute(sql) return cursor.fetchone()改完之后错误消失了,因为字符串拼接确实能绕过参数类型的问题。但 SQL 注入的入口也就此打开。AI 不会告诉你这一点,因为在它的判断里,"报错消失"就是任务完成。
我实测过多次,用 Cursor 和 Claude Code 修类似的数据库报错,大约有三分之一的概率它会选择字符串拼接而不是修正参数化查询的用法。这个比例在涉及动态表名、动态排序字段的场景下会更高,因为参数化查询确实不支持这些场景,AI 就会"聪明地"改用拼接。
2.2 一个真实的排查过程
上个月我帮朋友看一个项目,他用 AI 修了一个"用户搜索接口返回结果不对"的 Bug。修完之后功能正常,但我在做代码审计时发现搜索接口的 SQL 变成了这样:
def search_users(keyword, order_by): sql = f"SELECT * FROM users WHERE username LIKE '%{keyword}%' ORDER BY {order_by}" cursor.execute(sql) return cursor.fetchall()两个注入点:keyword直接拼接,order_by直接拼接。前者可以用万能密码绕过登录,后者可以用于盲注。
我问他:"你知道这里能注入吗?"他说:"AI 说这样改最简单,我就没多想。"
这个案例的典型性在于:AI 给出的修复方案在功能层面完全正确,在安全层面完全错误,而它不会主动标注这个 trade-off。如果你不具备 SQL 注入的识别能力,就会直接采纳。
2.3 怎么建立自己的检查习惯
我的做法是,只要 AI 的修复涉及数据库查询语句的改动,一律人工复核。具体检查这几个点:
| 检查项 | 安全写法 | 危险信号 |
|---|---|---|
| 用户输入拼接 | 使用参数化查询占位符 | 出现 f-string 或 + 拼接 |
| 动态表名/字段名 | 白名单校验后拼接 | 直接拼接用户输入 |
| LIKE 查询 | 参数化 + 转义 % 和 _ | 直接拼接 % 通配符 |
| ORDER BY | 白名单映射 | 直接拼接排序字段 |
| IN 查询 | 动态生成占位符 | 拼接逗号分隔的字符串 |
对于 ORDER BY 和表名这类参数化查询不支持的场景,正确做法是白名单:
ALLOWED_ORDER_FIELDS = {"id", "username", "created_at"} def search_users(keyword, order_by): if order_by not in ALLOWED_ORDER_FIELDS: order_by = "id" sql = f"SELECT * FROM users WHERE username LIKE %s ORDER BY {order_by}" cursor.execute(sql, (f"%{keyword}%",)) return cursor.fetchall()提示:AI 修完数据库相关 Bug 后,不要只看功能是否正常,一定要打开修改后的代码,搜索
execute、query、raw这些关键词,确认没有字符串拼接的用户输入。
2.4 一个容易被忽略的变体:ORM 的 raw 方法
现在很多项目用 ORM,AI 修 Bug 时如果发现 ORM 查询报错,有时会建议"用 raw SQL 绕过"。比如 Django 的raw()、SQLAlchemy 的text()、Sequelize 的sequelize.query()。这些方法本身没问题,但 AI 生成的 raw SQL 里经常带拼接。
我见过最隐蔽的一次是 AI 把 ORM 查询改成了Model.objects.raw(f"SELECT * FROM table WHERE id = {user_id}"),理由是"ORM 的 filter 在这个场景下有性能问题"。功能正常,但注入点也埋下了。
3. 第二类翻车:鉴权与校验被当成"干扰项"删掉
3.1 AI 的"排除干扰"逻辑有多危险
修 Bug 时一个常用手段是"注释掉可疑代码,看问题是否复现"。AI 也学会了这一招,而且用得比人更激进——它会直接删除或注释掉它认为"与当前 Bug 无关"的代码,包括鉴权中间件、参数校验、CSRF 保护、CORS 限制。
我遇到过一次典型情况:一个接口报 403,AI 分析后认为"是权限校验中间件导致的",建议先注释掉中间件确认问题。我照做了,问题确实定位到了。但 AI 在后续的修复方案里,没有提醒我恢复中间件,而是直接给了一个"绕过权限校验"的修复代码。如果我当时没注意,这个接口就会变成无鉴权接口。
这类问题的危险在于:AI 把安全机制当成了功能 Bug 的一部分。在它看来,403 是错误,消除 403 就是修复。但 403 可能是正确的安全行为,真正要修的是权限配置,而不是鉴权逻辑本身。
3.2 参数校验被删的几种常见形态
除了鉴权,参数校验也是重灾区。AI 修 Bug 时经常把校验逻辑删掉,因为校验失败会抛异常,而异常就是它要消除的目标。
常见的形态有这几种:
- 类型校验被删:比如
if not isinstance(user_id, int)被删掉,导致后续可以传入任意类型 - 范围校验被删:比如
if page_size > 100被删掉,导致可以请求超大分页拖垮数据库 - 格式校验被删:比如邮箱、手机号的正则校验被删掉,导致脏数据入库
- 必填校验被删:比如
if not username被删掉,导致空值进入业务逻辑
我印象最深的一次是 AI 修一个"文件下载接口报错"的 Bug,它把文件路径的校验删掉了,理由是"这个校验导致了路径拼接错误"。修完之后接口正常,但路径穿越漏洞也来了——用户可以通过../../etc/passwd下载任意文件。
3.3 修复后的自查清单
每次 AI 修完 Bug,我会对照这个清单检查一遍:
- 鉴权相关:修改的文件里有没有
auth、permission、middleware、guard这些关键词?如果有,确认逻辑没有被削弱。 - 校验相关:有没有
validate、check、verify、assert这些函数被删除或注释?如果有,确认是合理的。 - 异常处理:有没有
try/except被改成pass或者except Exception: pass?这种改动经常掩盖真实错误。 - 配置相关:有没有
DEBUG、CORS、CSRF相关的配置被改动?AI 有时会建议"关闭这些来排查问题"。
注意:AI 建议"临时注释掉某段代码来排查"时,一定要在修复完成后确认这段代码已经恢复。我习惯在注释的地方加一个
# TODO: RESTORE标记,修完搜索这个标记。
3.4 一个反直觉的经验:AI 越"聪明"越危险
用得久了你会发现,能力越强的 AI 模型,在这类问题上反而越危险。因为它更擅长"理解你的意图",当你表达"这个接口报 403 帮我修一下"时,它会倾向于直接给你一个能绕过 403 的方案,而不是告诉你"403 可能是正确的,你应该去检查权限配置"。
能力弱的模型可能只会给你一些通用建议,反而不会主动删代码。所以我的经验是:用强模型修 Bug 时,提示词里要明确加上"不要删除任何鉴权和校验逻辑,如果需要改动请先说明原因"。这一句话能挡掉很多问题。
4. 第三类翻车:错误处理把异常"吞"掉了
4.1 从"报错消失"到"问题被隐藏"
AI 修 Bug 最擅长的操作之一,就是把报错"吞掉"。原始代码抛异常,AI 给你包一层try/except,异常没了,功能"正常"了。但这种修复方式在安全上的隐患极大。
最典型的场景是登录接口。原始代码在密码错误时抛异常,AI 修完之后变成:
def login(username, password): try: user = get_user(username) if user.password != password: raise Exception("密码错误") return generate_token(user) except Exception: return {"code": 500, "msg": "登录失败"}看起来没问题,但仔细看:所有异常都被统一成了"登录失败"。这意味着数据库连接失败、用户不存在、密码错误、token 生成失败,返回的都是同一个响应。攻击者可以通过响应差异来判断用户是否存在,也可以通过大量请求来探测系统状态。
更危险的是,有些 AI 生成的代码会直接except: pass,把异常完全吞掉。这种情况下,如果鉴权逻辑抛异常,程序会继续往下执行,相当于鉴权被绕过了。
4.2 异常吞掉的三种危险模式
我总结了一下,AI 修 Bug 时吞异常主要有三种模式,危险程度递增:
第一种:宽泛捕获,统一返回。就是上面登录接口的例子,except Exception捕获所有异常,返回统一错误。危险在于信息泄露和状态探测。
第二种:捕获后继续执行。比如:
try: check_permission(user, resource) except Exception: pass do_something(resource)权限校验失败被吞掉,代码继续执行。这是最危险的模式,直接导致鉴权绕过。
第三种:捕获后返回默认值。比如:
try: role = get_user_role(user_id) except Exception: role = "admin"异常时返回默认值,如果默认值是高权限,就是提权漏洞。
4.3 怎么判断 AI 的异常处理是否安全
我的判断标准很简单:异常被捕获后,程序的执行路径是否发生了变化。如果异常被吞掉后,程序继续执行了原本不该执行的逻辑,就是危险的。
具体检查方法:
- 搜索 AI 修改后的代码里的
except、catch、rescue关键词 - 看每个异常处理块里,是
pass、return、raise还是继续执行 - 如果是
pass或继续执行,确认被跳过的逻辑是否包含安全检查 - 如果是
return,确认返回的值是否泄露了敏感信息
对于登录、支付、权限相关的接口,我的原则是异常必须显式处理,不能吞掉。该抛的抛,该记日志的记日志,该返回通用错误的返回通用错误,但绝不能pass。
4.4 一个实用的提示词技巧
在让 AI 修 Bug 时,我会在提示词里加一句:"如果需要添加异常处理,请确保异常不会被静默吞掉,并且不要改变原有的执行路径"。这句话能显著降低 AI 吞异常的概率。
另外,如果 AI 给出的修复方案里有except: pass或者catch (e) {}这种空异常处理,我会直接要求它重写,并说明"这个异常处理会掩盖真实问题,请改成记录日志并重新抛出"。
5. 把安全检查嵌进 AI 修 Bug 的工作流
5.1 修 Bug 前:给 AI 划好红线
与其修完之后提心吊胆地检查,不如在修之前就把红线划好。我现在用 AI 修 Bug 的标准提示词模板是这样的:
帮我修复以下 Bug:[Bug 描述] 约束条件: 1. 不要删除或注释任何鉴权、校验、异常处理逻辑 2. 不要将参数化查询改为字符串拼接 3. 如果需要改动安全相关代码,请先说明原因和影响 4. 修复后请列出所有修改的文件和具体改动点 5. 如果修复方案涉及安全权衡,请明确标注这五条约束能挡掉大部分常见问题。特别是第 4 条,要求 AI 列出改动点,方便我快速定位需要复核的地方。
5.2 修 Bug 中:盯住三类高危改动
AI 修 Bug 的过程中,有三类改动需要特别警惕,我称之为"高危改动三件套":
- 数据库查询语句的改动:涉及 SQL 拼接、raw 查询、ORM 的 raw 方法
- 鉴权和校验逻辑的改动:涉及中间件、拦截器、装饰器、校验函数
- 异常处理的改动:涉及 try/except、错误返回、默认值
这三类改动出现时,不管 AI 说得多有道理,都要人工复核一遍。我的经验是,这三类改动占 AI 修 Bug 总改动的比例不到 20%,但引发的安全问题占 80% 以上。
5.3 修 Bug 后:用工具做一次快速审计
人工复核之外,我还会用一些工具做快速审计。对于 Python 项目,bandit是个不错的选择:
pip install bandit bandit -r ./your_project -f json -o report.json它会扫描 SQL 注入、硬编码密码、不安全的反序列化等问题。对于 JavaScript 项目,eslint-plugin-security可以集成到 ESLint 里:
npm install --save-dev eslint-plugin-security然后在.eslintrc里加上:
{ "plugins": ["security"], "extends": ["plugin:security/recommended"] }这些工具不能替代人工审计,但能帮你快速发现 AI 引入的明显问题。我通常是在 AI 修完 Bug 后跑一遍,看看有没有新增的高危告警。
5.4 一个我用了半年的检查表
最后分享一个我用了半年的检查表,每次 AI 修完 Bug 后对照着过一遍,五分钟就能完成:
| 检查项 | 检查方法 | 通过标准 |
|---|---|---|
| SQL 拼接 | 搜索execute、query、raw | 无用户输入拼接 |
| 鉴权逻辑 | 搜索auth、permission、middleware | 逻辑未被削弱 |
| 参数校验 | 搜索validate、check、assert | 校验未被删除 |
| 异常处理 | 搜索except、catch、pass | 无静默吞异常 |
| 敏感配置 | 搜索DEBUG、CORS、CSRF | 配置未被放宽 |
| 文件操作 | 搜索open、read、write | 路径未被拼接 |
| 命令执行 | 搜索exec、system、eval | 无用户输入拼接 |
这个检查表覆盖了我遇到过的 90% 以上的 AI 修 Bug 安全问题。刚开始用的时候会觉得麻烦,用久了就形成肌肉记忆了,看到 AI 的修复代码会自动去这几个点扫一眼。
5.5 关于 AI 修 Bug 的一个心态调整
用了大半年 AI 编程工具,我最大的体会是:AI 是一个能力很强但安全意识为零的初级开发者。它能快速定位问题、写出能跑的代码,但它不理解"为什么这段代码要这样写",也不理解"删掉这段代码会有什么后果"。
所以正确的用法不是"让 AI 修完就完事",而是"让 AI 做初稿,自己做安全审查"。把 AI 当成一个效率工具,而不是一个可以完全信任的开发者。这个心态调整过来之后,我用 AI 修 Bug 的效率反而更高了,因为我知道哪些地方需要重点看,哪些地方可以放心。
提示:如果你所在的团队有代码审计流程,建议把"AI 修改的代码"作为一个单独的审计类别,重点检查上面提到的三类高危改动。我见过太多团队因为"AI 改的应该没问题"而跳过了审计,最后出了安全事故。
6. 几个我踩过的具体坑和修复过程
6.1 一次 SQL 注入的完整排查链路
前面提到的搜索接口注入,我完整排查了一遍,过程值得记录。
第一步:发现异常。我在做代码审计时,看到search_users函数里有 f-string 拼接 SQL,第一反应是"这里有问题"。
第二步:确认可利用性。我构造了一个请求,keyword传入' OR '1'='1,返回了所有用户数据。确认存在 SQL 注入。
第三步:追溯引入时间。用git log -p查看这个文件的修改历史,发现是三天前一次 AI 修 Bug 时引入的。提交信息是"修复搜索接口返回结果不对的问题"。
第四步:评估影响范围。搜索接口是公开接口,不需要登录就能访问。这意味着任何用户都可以利用这个注入点读取数据库。影响范围是整个用户表。
第五步:修复。改回参数化查询,order_by加白名单校验:
ALLOWED_ORDER_FIELDS = {"id", "username", "created_at"} def search_users(keyword, order_by): if order_by not in ALLOWED_ORDER_FIELDS: order_by = "id" sql = f"SELECT * FROM users WHERE username LIKE %s ORDER BY {order_by}" cursor.execute(sql, (f"%{keyword}%",)) return cursor.fetchall()第六步:验证。重新构造注入请求,返回正常结果,注入不再生效。
第七步:复盘。这次问题的根源是 AI 修 Bug 时把参数化查询改成了拼接,而我没有在修完后复核。之后我把"数据库相关改动必须复核"加进了工作流。
6.2 一次鉴权绕过的发现过程
另一个案例是鉴权绕过。一个后台接口报 403,AI 修完后接口正常了,但我发现它把鉴权中间件注释掉了。
发现过程:我在 review 代码时,看到中间件注册的地方有一行被注释了,注释内容是# app.use(auth_middleware)。这个改动很隐蔽,因为注释掉一行代码不会影响其他功能,接口确实能正常访问了。
排查:我搜索了这个中间件的定义,发现它负责校验 JWT token。注释掉之后,所有请求都不需要 token 就能访问后台接口。
修复:恢复中间件,然后去查为什么这个接口会报 403。最后发现是 token 的权限字段配置错了,修正配置后接口正常,鉴权也保留了。
教训:AI 修 Bug 时,如果它建议"注释掉某段代码来排查",一定要在修完后确认这段代码恢复了。我现在的做法是,AI 给出的修复方案里如果有注释掉的代码,我会要求它明确说明"这段代码是否需要恢复"。
6.3 一次异常吞掉导致的提权
第三个案例比较隐蔽。一个用户信息更新接口,AI 修了一个"更新失败"的 Bug,加了一段异常处理:
def update_user(user_id, data): try: user = get_user(user_id) if user.role != "admin": raise PermissionError("无权限") update(user, data) except Exception: return {"code": 500, "msg": "更新失败"}问题在于,except Exception把PermissionError也捕获了。普通用户调用这个接口时,权限校验失败,但异常被吞掉,返回"更新失败"。看起来没问题,但实际上update函数没有被执行,所以没有提权。
但如果 AI 把代码改成这样:
def update_user(user_id, data): try: user = get_user(user_id) if user.role != "admin": raise PermissionError("无权限") except Exception: pass update(user, data)权限校验失败被吞掉,update继续执行,普通用户就能更新任意用户信息。这是典型的提权漏洞。
发现过程:我在做代码审计时,看到except Exception: pass这种写法,顺着看下去发现了问题。
修复:把异常处理改成显式捕获,权限异常单独处理:
def update_user(user_id, data): user = get_user(user_id) if user.role != "admin": raise PermissionError("无权限") update(user, data)教训:异常处理不能宽泛捕获,尤其是涉及权限校验的场景。except Exception会捕获所有异常,包括那些本应该中断执行的异常。
7. 写在最后:AI 修 Bug 的正确打开方式
用了这么久 AI 编程工具,我的结论是:AI 修 Bug 的能力毋庸置疑,但它需要一套配套的安全检查流程才能放心用。这套流程不复杂,核心就是三件事:
第一,修之前划红线。在提示词里明确告诉 AI 不要动鉴权、校验、异常处理,不要改参数化查询。这一句话能挡掉大部分问题。
第二,修之后查三类改动。数据库查询、鉴权校验、异常处理,这三类改动必须人工复核。其他的改动可以放心一些。
第三,用工具兜底。bandit、eslint-plugin-security 这些工具能帮你快速发现明显问题,虽然不能替代人工,但能提高效率。
我现在的习惯是,AI 修完 Bug 后,先跑一遍工具扫描,再对照检查表过一遍,最后重点看三类高危改动。整个过程大概五到十分钟,但能避免大部分安全问题。
最后说一句,AI 修 Bug 引入安全问题,责任不在 AI,在于我们用 AI 的方式。把 AI 当成一个需要 review 的初级开发者,而不是一个可以完全信任的专家,很多问题就自然避免了。