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:越权访问证据
测试账号 A 请求自己的记录 1001 可以成功;把 ID 改为 1002 后,错误版本返回了测试账号 B 的虚构数据。
  • 图 3:过度日志证据
整改前日志包含完整 Prompt、邮箱和 Token;整改后仅记录请求 ID、状态码、接口名称、耗时和错误类型。

这不是在批评某个 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. 请求头中是否出现 Authorizationapi-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,搜索 Authorizationtokenapi-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