ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI生成代码上线前安全自查:凭据、注入、越权与依赖检查

AI生成代码上线前安全自查:凭据、注入、越权与依赖检查 1. 先说说为什么 AI 写的代码上线前必须单独过一遍安全过去一年我用 AI 辅助写了大概六七个中小型项目有内部工具、有对外接口服务也接过几个帮朋友救火的活。这些项目里AI 生成的代码占比从三成到八成不等。跑通业务逻辑这件事AI 现在确实很强一个 CRUD 接口、一套数据清洗脚本、一份定时任务描述清楚需求几分钟就能给你一版能跑的。但问题恰恰出在能跑这两个字上——能跑不等于能上线更不等于能扛得住真实流量和真实的人。AI 的产出有一个非常稳定的特征功能路径写得又快又完整非功能路径几乎全靠你补。异常分支、边界值、权限校验、密钥管理、日志脱敏、依赖版本这些地方它默认是不写就不写而这些东西恰好是安全工程师最关心的部分。更麻烦的是AI 生成的代码读起来特别有说服力。变量命名规整、注释写得漂亮、结构层次分明评审的时候人很容易被这种整洁感带着走扫两眼觉得没问题就合并了。我自己就吃过这个亏一段处理优惠券核销的逻辑AI 写得漂漂亮亮还贴心地加了注释说明校验用户是否已使用该券结果它校验的是这个优惠券 ID 是否被任何人用过而不是是否被当前用户用过。上线两天有人拿别人用过的券反复试直接把库存刷穿了。这个 bug 代码规范、单元测试、代码评审全都没拦住因为测试用例也是 AI 写的它照着错误的假设写了对的断言。所以我现在形成了一个固定习惯任何 AI 参与生成的代码在提测之前必须单独跑一轮安全自查不管这个项目多小、不管是不是内部用。内部工具被当成跳板、测试环境连着生产库、临时接口忘了加鉴权这些事在真实项目里出现的频率高得吓人。这篇就按我自己实际执行的流程把检查什么、怎么查、查完怎么改完整讲一遍不讲空泛的原则只讲能直接抄作业的东西。前端、后端、脚本、数据管道都适用你不需要是安全专家但需要有一套固定的动作清单。2. AI 生成代码的高频雷区先搞清楚敌人在哪2.1 为什么 AI 特别容易在这几个地方翻车理解成因比记住结论重要因为成因决定了你该把检查重心放在哪。AI 的代码是在海量公开仓库上训练出来的而这些仓库里本身就充斥着教学示例、Quick Start 片段和十年前的技术博客。教学示例的特点是省略一切与主题无关的东西——官方文档里那段连接数据库的代码密钥永远是明文的鉴权永远是省略的错误处理永远是print(e)。AI 学到的就是这套最小可运行的表达习惯它在生成时默认你在写 demo而不是在写生产系统。第二个成因是它只对你说了什么负责不对你没说什么负责。你让它写一个导出报表的接口它会写参数解析、写数据库查询、写文件生成、写返回下载链接但它不会主动问这个接口谁能调导出的数据范围要不要按当前用户过滤文件名会不会被用户控制。你明确要求它加权限控制它能加得很好甚至能识别出越权风险你不提它就当不存在。这个特性意味着检查清单的价值远大于单次对话的技巧——你要靠流程兜底而不是靠每次都想起来提醒它。第三个成因是依赖幻觉。AI 会推荐已经停止维护的库、会给出不存在的版本号、会混用两个库的 API也会习惯性选择老版本写法比如 Python 里requests关闭证书校验、Node 里用已经不推荐的加密调用方式。这类问题扫描器不一定报但一旦上线就是供应链层面的定时炸弹。2.2 按危害排个序我实际遇到过的六类问题下面这张表是我这两年做代码审查时按真实发生频次和危害程度整理出来的不含理论推演都是真见过的。问题类型典型表现危害等级自动化发现难度硬编码凭据密钥、Token、数据库密码直接写在源码或配置文件里极高低工具一扫就出认证授权缺失接口无鉴权、只校验登录不校验归属、管理员接口无角色判断极高高需人工梳理注入类问题SQL 字符串拼接、命令拼接、模板拼接高中规则能覆盖大部分依赖与供应链引入废弃库、锁文件缺失、镜像基础层老旧高低信息泄露报错直接回显堆栈、日志打印敏感字段、接口返回全量字段中中配置类问题调试开关未关、跨域放开、内网地址暴露、目录列表开启中低排序的依据很简单前两类一旦出问题攻击者不需要任何技巧就能拿到数据后面几类通常需要组合利用但也不能放过。特别说明一下第二类AI 代码在这方面的问题最隐蔽因为代码看起来有鉴权——它确实调了登录校验中间件只是没做资源归属判断。登录校验解决的是你是谁归属校验解决的是这东西是不是你的两者差一个量级。2.3 一句提醒不要指望 AI 自查 AI我试过让同一个模型去审查自己刚写的代码效果有限。它的自查倾向于检查语法、逻辑连贯性、命名规范这些显性质量对安全边界的敏感度明显不足而且它对自己写过的结构有路径依赖容易得出看起来没问题的结论。让另一个模型交叉审查会好一些但也只能当辅助。真正有效的是确定性的工具加上人的清单式核对。工具负责找模式化的东西人负责判断业务上下文这个分工不能颠倒。3. 上线前的安全体检清单按优先级一步步来3.1 第一优先级把凭据从代码里彻底清出去这件事必须排第一位因为它的修复成本最低、收益最高而且一旦泄露就没法挽回——代码进了仓库历史记录里就永远有。AI 生成代码时最喜欢干的事就是在配置里写一句API_KEY sk-xxxxxxxx或者给你一段能跑的连接串密码明文。我的做法是三层防护。第一层是提交前拦截。在项目根目录放一个.pre-commit-config.yaml让密钥扫描在git commit阶段就跑起来不合格直接拒绝提交。# .pre-commit-config.yaml repos: - repo: https://github.com/gitleaks/gitleaks rev: v8.18.4 # 版本号建议按当前最新稳定版填写 hooks: - id: gitleaks第二层是全量扫描包括历史提交。改掉当前文件里的密钥没用历史提交里还在。# 扫描工作区和全部提交历史--redact 让输出里的密钥打码避免二次泄露 gitleaks detect --source . -v --redact # 只关心确认有效的凭据噪音更小 trufflehog git file://. --only-verified注意扫描报告本身可能包含明文凭据别把报告文件直接丢进群里或者工单系统。用--redact或者扫描完立刻处理掉输出文件。第三层是改造代码把凭据全部外置。判断标准是源码和镜像里不允许出现任何能直接用于认证的字符串。改造方式按运行环境选容器化项目用环境变量注入本地开发用.env文件加.gitignore云上用密钥管理服务。.env文件一定要确认在.gitignore里我见过.env被提交上去、扫描器报了、开发者回复这是本地文件没事的情况——版本库里没有本地文件这回事。import os # 反例AI 最常给出的写法 # DB_PASSWORD Prod2024! # JWT_SECRET secret # 正例启动时读取缺失直接崩避免带着空配置跑起来 DB_PASSWORD os.environ[DB_PASSWORD] JWT_SECRET os.environ[JWT_SECRET] if len(JWT_SECRET) 32: raise RuntimeError(JWT_SECRET 长度不足拒绝启动)这里加一个长度校验是有意为之。很多团队把密钥外置了但值用的是123456或者项目名外置了等于没外置。启动即校验、不满足就崩溃比上线后被人爆破要好得多。3.2 第二优先级输入校验和注入类问题注入类问题是 AI 代码的重灾区原因前面说过——教程示例为了简洁大量使用字符串拼接。SQL 注入只是其中一种还有命令注入、模板注入、路径穿越、XXE、反序列化本质都是用户输入被当成了代码或指令执行。检查方法上自动化规则能覆盖大部分常见模式。Semgrep 用社区规则集就能扫出很多bandit专门针对 Pythongosec针对 Go前端用eslint-plugin-security。跑一遍基础扫描# 通用规则 自定义规则--error 让发现高危问题时返回非零退出码 semgrep --configauto --error . # Python 专项-ll 只报中危以上 bandit -r . -ll # Node 项目 npm audit --production但自动化只能解决一半剩下那一半得靠人看。我的做法是搜关键词把可疑调用点全部列出来逐个确认。这些关键词包括execute、query、raw、system、popen、subprocess、eval、exec、render_template_string、pickle.loads、yaml.load、open(。搜出来之后看两件事参数是不是用户可控的拼接方式是不是安全的。# 反例AI 生成的看起来很正常的查询 def get_order(order_id): sql fSELECT * FROM orders WHERE id {order_id} return db.execute(sql).fetchone() # 正例参数化查询同时限定返回字段 def get_order(order_id): sql SELECT id, amount, status, created_at FROM orders WHERE id %s return db.execute(sql, (order_id,)).fetchone()正例里我顺手把SELECT *改成了列名。原因是很多表里躺着手机号、身份证号、内部备注这些不该给前端的字段SELECT *一写序列化的时候全出去了这就是典型的信息泄露。AI 生成查询时默认用*这个习惯要改。命令执行同样AI 经常给出subprocess.run(cmd, shellTrue)这种写法只要参数里有用户输入就是命令注入。# 反例 subprocess.run(fffmpeg -i {user_path} out.mp4, shellTrue) # 正例列表传参不走 shell加超时 subprocess.run( [ffmpeg, -i, user_path, out.mp4], shellFalse, checkTrue, timeout60, )反序列化这块要单独点一下。AI 在处理缓存、消息队列、配置文件时经常给你pickle.loads或者yaml.load(data)。前者在 Python 里等于把执行权交出去后者不加Loader参数同样危险。看到这两个调用直接改成json.loads或yaml.safe_load没有例外。3.3 第三优先级认证、授权和越权AI 最容易忘的一环这类问题自动化工具基本查不出来因为它需要理解业务语义。我用的方法是接口清单法把所有路由导出来逐个问三个问题——谁能访问、能看到哪些数据、能改哪些数据。三个问题里任何一个答不上来就是风险点。具体到这个环节最有效的技术手段是强制在查询层加归属条件而不是在业务逻辑里判断。因为业务逻辑判断容易漏分支数据库条件不会。# 反例先查再判断中间有无数种绕过方式也容易漏 order db.query(SELECT * FROM orders WHERE id %s, (order_id,)) if order[user_id] ! current_user_id: raise PermissionError # 正例把归属条件写进查询本身查不到就是查不到 row db.query( SELECT id, status FROM orders WHERE id %s AND user_id %s, (order_id, current_user_id), ) if row is None: return {code: 404, msg: not found}正例里还包含一个小技巧越权访问返回 404 而不是 403。403 等于告诉对方这个资源存在但不属于你这在批量探测时是有效信息泄露。404 什么都不透露。另外几个必须人工确认的点批量接口有没有做数量限制AI 生成的接口经常允许一次传几百个 ID直接变成数据遍历工具分页接口的页码和页大小有没有上限导出和下载接口用的是不是自增 ID 作为文件名改成随机 UUID 或者带签名的短时效链接管理后台的接口是不是只在前端做了角色隐藏前端隐藏等于没做。3.4 第四优先级依赖、配置和部署环境依赖这块AI 的坑主要集中在两个地方一是给你一个不存在的包名或版本号二是推荐一个已经很久没更新的库。前者构建时会报错反而安全后者能装上、能跑但躺在那里带漏洞。# Python pip-audit -r requirements.txt # 通用支持多语言锁文件 osv-scanner -r . # 文件系统层面同时查依赖漏洞、密钥、配置错误 trivy fs --scanners vuln,secret,misconfig --exit-code 1 . # 镜像层面别忘了基础镜像自己也有漏洞 trivy image your-registry/app:1.0.0这里有个实际经验扫描出来的漏洞不用全部修按可被远程利用 无需认证 有公开利用方式三个条件筛把这类清掉剩下的排期处理。全部清干净在现实项目里不现实但优先级必须清楚。配置类问题同样是 AI 和人工都容易忽略的因为它们在开发环境里都是对的。重点检查这么几项调试开关。Flask 的debugTrue、Django 的DEBUGTrue、Node 的NODE_ENVdevelopment上线必须是关闭状态。调试模式开着等于把源码和堆栈直接送给访问者。跨域配置。AI 生成的代码里Access-Control-Allow-Origin: *出现频率极高尤其是前后端分离项目。如果接口需要携带凭据通配符本身就是禁止的必须换成白名单。报错处理。全局异常处理器有没有兜底会不会把堆栈、SQL 语句、文件路径回显给前端。日志。日志里有没有打印手机号、身份证、Token、完整请求体。日志系统的访问权限往往比数据库松得多。目录列表和静态服务。有没有把整个项目目录或者上传目录直接暴露成静态资源。容器与端口。数据库、缓存、管理端口有没有意外映射到宿主机或者公网。4. 实操一次完整的上线前安全检查怎么跑4.1 工具准备和统一入口工具散着用很容易漏我习惯在项目里放一个Makefile把所有检查串成一条命令任何时候都能一键跑完也方便接进流水线。.PHONY: sec sec-fast sec: echo 1/5 密钥扫描 gitleaks detect --source . -v --redact echo 2/5 依赖漏洞 pip-audit -r requirements.txt echo 3/5 静态规则 semgrep --configauto --error --quiet . echo 4/5 Python 专项 bandit -r . -ll -q echo 5/5 配置与容器 trivy fs --scanners vuln,secret,misconfig --exit-code 1 . # 提交前跑的轻量版只查最关键的两项 sec-fast: gitleaks protect --staged -v semgrep --configauto --error --quiet .两个入口的区别要讲清楚sec是发版前跑的全量检查可能要几分钟sec-fast是每次提交前跑的只查密钥和规则命中控制在十秒内否则没人愿意用。工具的第一原则是快慢一点的检查一定会被人用--no-verify绕过。4.2 自定义规则把 AI 的坏习惯固化成检查项通用规则集覆盖不全 AI 特有的坏习惯所以我会维护一份自己的规则文件。Semgrep 的规则写起来不复杂把上面提到的几个高频模式固化下来比人肉搜索可靠得多。# .semgrep/ai-common.yml rules: - id: python-verify-false languages: [python] severity: ERROR message: 关闭了 TLS 证书校验禁止上线 patterns: - pattern: requests.$M(..., verifyFalse, ...) - id: flask-debug-on languages: [python] severity: ERROR message: Flask 以调试模式启动禁止上线 patterns: - pattern: $APP.run(..., debugTrue, ...) - id: yaml-unsafe-load languages: [python] severity: ERROR message: 使用不安全的 YAML 加载方式 patterns: - pattern: yaml.load(...) - pattern-not: yaml.load(..., Loaderyaml.SafeLoader) - pattern-not: yaml.load(..., Loaderyaml.CSafeLoader) - id: js-cors-wildcard languages: [javascript, typescript] severity: WARNING message: 跨域配置使用了通配符确认是否允许携带凭据 patterns: - pattern: $RES.setHeader(Access-Control-Allow-Origin, *)semgrep --config.semgrep/ai-common.yml --error .这份规则我建议从三到五条起步边踩坑边加。不要一上来写几十条误报太多会让人直接不看了。4.3 人工审计的四个切口自动扫描跑完接下来是人的活。我不会逐行读代码那样效率太低我用四个切口去定位重点区域。第一个切口是入口。把所有对外暴露的路由、消息队列消费者、定时任务、命令行参数列出来每个入口问一遍输入从哪来、有没有长度和格式限制、有没有鉴权。AI 生成的定时任务特别容易漏掉因为它看起来不对外但很多定时任务会读取数据库里的任务表任务参数如果可写就是一条越权链路。第二个切口是数据出口。哪些接口会返回数据、返回的字段是怎么来的。如果是SELECT *加自动序列化基本可以确定有字段泄露。我会故意构造一个请求看返回体里有没有不该出现的字段这个动作花不了两分钟。第三个切口是外部调用。所有出网的请求都要看目标地址是硬编码的还是用户可控的、有没有超时、有没有跟随重定向、会不会访问内网地址。AI 写爬虫和 webhook 转发时经常直接把用户传进来的 URL 丢给请求库这就是标准的服务端请求伪造。from urllib.parse import urlparse import requests ALLOW_HOSTS {api.partner.com, cdn.example.com} def safe_fetch(url: str) - str: p urlparse(url) if p.scheme not in (http, https): raise ValueError(scheme not allowed) if p.hostname not in ALLOW_HOSTS: raise ValueError(host not allowed) # allow_redirectsFalse 很关键否则白名单会被 302 绕过 resp requests.get(url, timeout5, allow_redirectsFalse) resp.raise_for_status() return resp.text第四个切口是文件操作。所有涉及上传、下载、解压、读写的代码都过一遍。检查点文件名是不是用户可控路径穿越、文件类型是按扩展名还是按内容判断扩展名可以伪装、解压有没有做总大小和文件数量限制解压炸弹、存储路径是不是在 Web 根目录之外。4.4 修复与回归别只改一处找到问题之后修复有两个原则。第一同类问题一并改完。发现一个接口存在越权就要把所有同类接口筛一遍因为 AI 生成代码的一致性很强它犯的错往往是一整套。第二修复要写进测试。AI 写的测试只覆盖正常路径要给它补上异常路径的用例尤其是权限相关。一个用户 A 不能读到用户 B 的订单的测试用例比十条规范文档有用。改完之后重新跑一遍make sec然后重点看两件事一是原来报的问题是否消失二是有没有引入新的告警。有时候为了绕过扫描器报错开发者会把拼接改成另一种拼接看起来不报了实际上换个入口还能利用。5. 常见问题与排查技巧实录5.1 扫描器报了一堆怎么分辨真假刚接入工具的时候第一次跑通常几百条告警看两眼就放弃了。我自己的处理顺序是这样的先按严重级别过滤只看高危再按是否在对外接口的调用链上过滤不在链上的降级最后按输入是否用户可控过滤。三轮下来通常只剩十几条需要真正处理。误报集中在这几类测试代码和数据构造脚本可以加白名单排除目录、常量拼接的查询参数是常量不是用户输入、内网工具里的宽松配置。这些可以在规则里加排除但排除规则要写清楚原因加一行注释说明为什么忽略否则半年后没人敢动。漏报更麻烦也更值得警惕。工具查不出业务逻辑漏洞查不出权限设计缺陷查不出跨接口的组合利用。所以我的做法是工具的结论只作为可以进入下一阶段的门槛不作为安全的证明。5.2 那些工具永远查不出来的问题说几个具体的。第一个是状态机缺陷。AI 写订单、工单、审批这类流程代码时往往只校验当前状态能不能执行某个动作不校验操作人。比如已取消的订单可以退款这条规则它写对了但谁都能触发取消再退款。第二个是并发问题。优惠券、库存、余额这类涉及扣减的逻辑AI 默认写成先查再改没有任何锁或者原子操作。# 反例先查后改并发下必然超卖 stock db.query(SELECT stock FROM items WHERE id %s, (item_id,))[stock] if stock 0: db.execute(UPDATE items SET stock %s WHERE id %s, (stock - 1, item_id)) # 正例把判断放进更新语句靠数据库保证原子性 affected db.execute( UPDATE items SET stock stock - 1 WHERE id %s AND stock 0, (item_id,), ) if affected 0: raise RuntimeError(库存不足)第三个是幂等性。支付回调、消息重投、用户连点这些场景 AI 基本不会主动处理。解决办法是在关键写操作上加唯一约束或者幂等键让重复请求在数据库层被拒绝而不是靠业务代码里的 if 判断。5.3 常见问题速查表现象可能原因定位方法处理方式扫描器报密钥泄露硬编码或历史提交残留gitleaks detect --source .外置到环境变量历史提交需重写并轮换密钥接口返回大量不需要的字段SELECT *加自动序列化抓包看响应体显式列出字段建响应模型白名单同一资源换个 ID 就能访问缺少归属校验用两个账号交叉请求把归属条件写进查询语句上传后能访问到非预期文件文件名用户可控传../或双扩展名测试随机文件名扩展名走白名单映射依赖扫描报高危锁文件缺失或基础镜像老旧trivy fs、npm audit锁定版本更新基础镜像按可利用性排期生产报错回显堆栈调试模式未关或缺少全局异常处理故意触发一次异常关闭调试统一异常响应结构日志里出现敏感数据直接打印请求体或对象检索日志关键词脱敏中间件禁止打印完整请求体5.4 我自己踩过的几个坑第一个坑是以为加了.gitignore就安全了。实际上.env被提交、删除、再提交历史记录里依然有。判断标准是有没有进过版本库进过就得当作已泄露处理光删文件不够要么重写历史要么直接轮换密钥。轮换更省事也更可靠。第二个坑是过度依赖扫描结果把扫描通过当成了放行条件。有一次扫描全绿上线后还是被薅了一波问题出在一个看起来无关紧要的查询接口上——它没有做分页限制攻击者用一个循环把整张用户表拉走了。这种功能正常但设计有问题的情况工具永远发现不了只能靠人对着接口清单问一遍。第三个坑是只在发版前检查中间几周的开发过程完全裸奔。后来我把密钥扫描放到了提交阶段把静态规则接到了流水线的每次构建上只有深度审计留在发版前。做安全这件事检查点的位置比检查的强度更重要越靠前成本越低。最后分享一个小习惯每次让 AI 生成一段涉及数据库、文件、网络、权限的代码后我会立刻在同一个对话里追加一句这段代码在生产环境上线前需要做哪些安全加固请逐条列出。它给出的清单不一定完整经常漏掉越权和并发但当作自查的提示词是够用的能让我在写代码的当下就把大部分低级问题挡掉剩下的靠上面的流程兜底。
返回列表