AI 写的应用能跑,却差点带着 3 个漏洞上线:一份公网部署前安全体检
AI 写的应用能跑,却差点带着 3 个漏洞上线:一份公网部署前安全体检
域名已经解析好了,我准备把链接发进群里,最后随手打开了一次浏览器 Network 面板。
这一眼,让上线按钮又安静地躺了回去。
这是一个用 AI 辅助生成的小应用:用户可以注册登录、发起 AI 对话,也能查看历史记录。本地测试时,页面正常、接口能通、数据库有数据,看起来已经到了“绑定域名,正式发布”的阶段。
但在上线前的检查中,我连续看到三个危险信号:
1. 浏览器请求里能够找到模型 API Key;
2. 修改历史记录 ID,账号 A 可以读取账号 B 的测试数据;
3. 日志保存了完整 Prompt、邮箱和部分请求凭据,数据库也保留了超出功能需要的原始内容。
最麻烦的是,这三个问题在正常使用页面时完全看不出来。
AI 很擅长把需求迅速变成“可以运行的代码”,但“能运行”和“能安全上线”,中间还隔着一次必须由人完成的验证。
本文把密钥泄露、越权访问和用户数据风险合并成一次完整体检。所有账号和数据均为虚构测试内容,截图中的测试密钥应在截图前完成作废。
数据说明:由于本文没有获得项目原始检查记录中的接口总数、整改用时等审计数据,因此不编造具体数字。实际发布案例时,应将本文截图位和检查记录替换为项目真实数据。
一、应用能跑,不等于能上线
这个应用的基本数据流并不复杂:
flowchart LR
U[用户浏览器] --> F[前端页面]
F --> B[应用后端]
B --> D[(数据库)]
B --> A[AI API]
问题出现在实际实现上。AI 为了快速完成对话功能,生成了一条更短的调用链:
flowchart LR
U[浏览器] -->|携带 API Key| A[第三方 AI API]
U --> B[应用后端]
B --> D[(数据库)]
功能确实能用,但密钥也被一起交给了浏览器。与此同时,历史记录接口只检查用户是否登录,没有确认被读取的记录究竟属于谁。
最后,开发阶段为了方便排错,日志几乎把请求原样保存了下来。
三个需要保留的证据画面
正式复盘时,建议保留以下经过脱敏的截图:
- 图 1:密钥泄露证据
Network 面板中,请求头出现 Authorization: Bearer sk-*。图注必须注明:仅为测试密钥,截图前已作废。*
- 图 2:越权访问证据
1001 可以成功;把 ID 改为 1002 后,错误版本返回了测试账号 B 的虚构数据。
- 图 3:过度日志证据
这不是在批评某个 AI 编程工具。无论使用 ChatGPT、Claude、Gemini,还是其他代码生成工具,模型都会优先满足显式需求。
你说“帮我实现历史记录查询”,它可能完成查询;但如果没有继续要求“只能查询当前用户的数据”,这条边界未必会自动出现。
二、第一个坑:密钥没写在页面上,也可能已经泄露
最常见的误区是:
我把 API Key 放进 .env 了,所以它是安全的。
.env 只是环境变量的存放方式,并不代表其中的内容一定只存在于服务端。
下面这段代码看似没有直接写死密钥,实际上仍然危险:
const apiKey = import.meta.env.VITE_API_KEY;
fetch("https://example.com/v1/chat", {
method: "POST",
headers: {
Authorization: Bearer ${apiKey},
"Content-Type": "application/json"
},
body: JSON.stringify({ message })
});
像 VITE_、NEXT_PUBLIC_ 这类面向前端暴露的环境变量,通常会进入客户端构建产物。浏览器要使用它,就必须先拿到它。
换句话说,交给前端的秘密,本质上已经不再是秘密。
它可能出现在:
- 浏览器
Network请求头或请求参数; - 打包后的 JavaScript 文件;
- Source Map;
- 浏览器本地存储;
- CI/CD 构建日志;
- Git 当前版本或历史提交。
如何检查密钥是否暴露
先打开浏览器开发者工具,完成一次 AI 对话,然后检查:
1. 请求是否直接发往第三方模型接口;
2. 请求头中是否出现 Authorization、api-key 等字段;
3. 请求参数、响应内容和错误信息中是否包含凭据;
4. 前端 JavaScript 文件中能否搜索到密钥片段。
再检查构建产物和代码仓库:
grep -R "VITE_API_KEY\|NEXT_PUBLIC_\|Bearer " dist build .next
Git 仓库可以使用 Gitleaks、TruffleHog 或 GitHub Secret Scanning 等工具扫描。
gitleaks detect --source . --redact
建议截图:展示扫描工具命中的文件路径和已打码的测试密钥,不要展示完整值,也不要拿仍然有效的生产密钥做演示。
正确结构:前端只调用自己的后端
前端不再持有模型密钥,只发送业务所需的数据:
await fetch("/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ message })
});
后端再从服务端环境变量读取密钥:
const response = await fetch(process.env.MODEL_API_URL, {
method: "POST",
headers: {
Authorization: Bearer ${process.env.MODEL_API_KEY},
"Content-Type": "application/json"
},
body: JSON.stringify({ message: req.body.message })
});
整改后的链路应该是:
flowchart LR
U[浏览器] -->|不携带模型密钥| B[自有后端]
B -->|服务端读取密钥| A[AI API]
B --> D[(数据库)]
但要注意,把密钥放到后端只是基础措施。还要设置调用额度、请求超时、频率限制和异常告警,否则自己的接口可能变成一个无门槛的模型代理。
如果你正在测试 AI 小应用,可以前往 api.884819.xyz 查看可用的接口接入方式,再通过自己的服务端完成调用。平台使用用户名和密码即可注册,不需要邮箱验证,内置 AI 对话功能;国产模型如 Deepseek、千问等可免费使用,其他服务没有月租和订阅,按量付费。
新用户注册即送体验token。无论使用哪种 API,都不要把访问密钥写进网页、公开仓库或客户端应用。
密钥已经泄露,删除代码还不够
如果密钥曾进入公开仓库、前端构建产物或浏览器请求,应当默认它已经泄露,并立即:
1. 作废旧密钥;
2. 重新生成密钥;
3. 清理 Git 历史及公开构建产物;
4. 检查调用记录和异常消耗;
5. 为新密钥设置最小权限;
6. 增加额度、限流和告警。
不能只删除当前代码。Git 历史、缓存、Fork、构建日志和搜索引擎缓存中,可能仍然留有副本。三、第二个坑:登录了,不代表只能访问自己的数据
密钥修完后,我用两个虚构测试账号检查历史记录:
- 账号 A:
test_a - 账号 B:
test_b
账号 A 正常请求:
GET /api/history/1001
返回自己的测试记录。
随后把对象 ID 改成:
GET /api/history/1002
错误版本仍然返回了账号 B 的记录。
问题就在这段查询:
const record = await db.history.findUnique({
where: { id: req.params.id }
});
后端可能已经通过 Session 或 Token 确认“请求者登录了”,但数据库查询只使用记录 ID,没有检查这条记录属于谁。
这类问题对应 OWASP API Security Top 10 中常见的 对象级授权失效,即 Broken Object Level Authorization。AI 生成 CRUD 接口时尤其容易出现,因为“根据 ID 查询、修改和删除”是最直接的实现。
不要信任前端传来的 user_id
下面这种修复仍然不可靠:
const record = await db.history.findFirst({
where: {
id: req.params.id,
userId: req.body.userId
}
});
因为前端传来的 userId 同样可以被修改。
用户身份应由服务端根据可信的 Session 或 Token 获取,并直接加入数据库查询:
const record = await db.history.findFirst({
where: {
id: req.params.id,
userId: req.user.id
}
});
if (!record) {
return res.status(404).json({ error: "Not found" });
}
修复后,账号 A 再请求账号 B 的对象 ID,应返回:
HTTP/1.1 404 Not Found
也可以根据业务设计返回 403 Forbidden。对于不希望暴露资源是否存在的接口,返回 404 往往更稳妥。
不要只修 GET 请求
对象归属校验必须覆盖所有数据操作:
- 读取单条记录;
- 获取列表;
- 修改标题或内容;
- 删除记录;
- 批量导出;
- 分享和复制;
- 文件下载;
- 管理后台接口。
否则就可能出现“不能看,但可以删”或“页面不能访问,导出接口却能下载”的半修复状态。
复测时至少准备两个测试账号,交叉请求彼此的对象 ID。不要只在前端点击按钮,要直接修改 URL、请求体和路径参数。
四、第三个坑:收集的数据越多,安全成本越高
数据库没有公开,不代表用户数据就是安全的。
一个 AI 应用可能无意中形成多份数据副本:
flowchart TD
U[用户输入] --> D[(业务数据库)]
U --> L[应用日志]
U --> A[AI API]
D --> B[数据库备份]
L --> P[日志或分析平台]
U --> E[错误堆栈]
常见内容包括:
- 完整对话和 Prompt;
- 邮箱、手机号、IP 地址;
- 上传文件及解析文本;
- Session、Token 或请求头;
- 包含用户输入的错误堆栈;
- 发送给第三方 API 的原始请求;
- 数据库备份和日志归档。
整改前,一条日志可能像这样:
[email protected]
prompt=请分析这份包含个人信息的文件……
token=Bearer eyJhbGciOi...
status=500
整改后,日志只保留排错真正需要的信息:
request_id=req_8f2c**
route=/api/chat
status=500
duration_ms=[实测值]
error_type=UPSTREAM_TIMEOUT
其中耗时应填写系统真实记录,不要为了让案例“更具体”而虚构数字。
数据最小化,不是少建几个字段
可以用四个问题逐项盘点:
1. 为什么收集?
是否有明确功能目的?
2. 是否必须保存?
能否只在当前请求中处理?
3. 需要保存多久?
是否存在清理周期?
4. 用户如何删除?
删除是否覆盖数据库、文件、日志索引和备份周期?
《个人信息保护法》强调处理个人信息应当具有明确、合理的目的,与处理目的直接相关,并采取对个人权益影响最小的方式;保存期限原则上也应当是实现处理目的所必要的最短时间。
对普通开发者而言,最直接的技术动作是:
- 非必要不收集;
- 非必要不保存原始 Prompt;
- 邮箱、手机号和 IP 在展示及日志中脱敏;
- 禁止记录密码、Token 和完整请求头;
- 设置数据保留与自动清理策略;
- 提供用户删除入口;
- 清楚告知数据是否会发送给第三方模型服务。
五、修完以后,用复测决定能不能上线
安全整改不能以“代码已经改了”为结束,而要以“原来的问题无法再次复现”为结束。
这次复测应得到三个结果:
- 浏览器、构建产物和 Git 扫描中不再出现有效模型密钥;
- 账号 A 无法读取、修改、删除或导出账号 B 的测试数据;
- 日志不再记录完整 Prompt、邮箱、Token 等非必要信息,删除流程能够实际执行。
可保存的上线检查表
| 检查项 | 最低上线标准 | 验证方法 | | API Key | 不进入前端、公开仓库和日志 | 搜索构建产物、Network、Git 历史 | | 用户权限 | 每次数据操作都校验资源归属 | 两个测试账号交叉请求 | | 接口滥用 | 有鉴权、限流、额度和超时 | 重复请求与未登录请求测试 | | 用户数据 | 只收集必要字段 | 检查数据库、日志和第三方平台 | | 数据删除 | 用户能够删除,备份有清理周期 | 发起一次完整删除测试 | | 错误日志 | 不包含 Token、密码和完整对话 | 主动制造错误并查看日志 | | 上线复测 | 修复后无法再次复现 | 保存请求与响应证据 |小白版:10 分钟快速检查
如果时间有限,至少完成以下动作:
1. 打开浏览器 Network,搜索 Authorization、token、api-key;
2. 搜索前端代码和构建产物中的密钥片段;
3. 准备两个账号,互换历史记录 ID;
4. 主动触发一次接口报错,查看日志内容;
5. 删除一条测试数据,确认它不再能通过其他接口读取。
进阶版:把安全检查变成上线门禁
团队项目可以把检查加入 CI/CD:
- 使用 Gitleaks 等工具扫描 Git 提交;
- 为每个资源接口编写跨账号权限测试;
- 对未登录、低权限和异常参数请求做自动化测试;
- 对日志字段设置白名单;
- 定期验证数据清理任务;
- 新增文件上传、登录方式或第三方服务后,强制重新评审。
OWASP API Security Top 10 中的对象级授权失效、身份验证问题和不受限制的资源消耗,往往不是“页面是否能用”可以验证的。OWASP 的凭据管理与日志安全建议也指向同一个原则:秘密不能交给不可信客户端,日志不能成为另一套敏感数据库。
先保存这张上线检查表,再检查一次自己的调用链。如果需要为测试项目接入 API,可访问 api.884819.xyz;接入时请坚持“前端不持有密钥、后端统一鉴权”的基本架构。
AI 生成的应用并非天然不安全。真正的风险,是我们把“功能完成”误认为“工程完成”。
这次体检解决了“别人能不能偷走密钥、读取数据”的问题,但上线后还有另一类更隐蔽的风险:如果有人批量调用接口、恶意刷模型额度,或者用超长输入拖垮服务,账单可能比漏洞更早找上门。
下一篇将实测 AI 小应用的限流、额度控制和防刷方案,并给出一套低成本配置模板。 本文由8848AI原创,转载请注明出处。关注8848AI,带你从零开始学AI。#AI安全 #AI编程 #API安全 #密钥管理 #Web开发 #人工智能 #8848AI