很多数据泄露事件,并不是因为开发者故意把敏感信息发布出去。真正危险的地方,往往藏在那些"看起来很合理"的代码设计里。
比如:
- 为了方便未来前端开发,API 直接返回完整用户对象。
- 为了方便排查问题,日志记录完整请求。
- 为了方便分享文件,下载链接包含文件地址。
- 为了分析用户行为,埋点收集完整表单内容。
- 为了临时使用,文件上传云存储后等待自动删除。
每一个决定都有自己的理由。系统需要可观察性、调试能力、灵活扩展、快速开发和更好的用户体验。问题并不在于这些功能本身,而在于:数据离开了原本受到保护的边界之后,是否仍然保持同样的安全级别。
例如:
- 数据库里安全保存的密码重置 Token,放入 URL 后可能进入浏览器历史记录。
- 受到严格权限控制的客户资料,导出文件后可能变成公开下载链接。
- API 没有返回的秘密信息,可能依然出现在日志中。
- 生产数据库保护完善的备份文件,可能长期无人管理。
因此,安全问题不能只问"数据有没有加密?",更应该问:数据去了哪里?谁能看到每一份副本?这些副本保存多久?这个功能真的需要这些数据吗?
1. API 直接返回完整数据库对象
这是最常见的数据泄露方式之一。很多开发者都会这样设计:从 数据库 查询用户,返回用户对象,框架自动序列化成 JSON,接口完成。代码非常少,看起来非常高效。
例如:
数据库 User 对象
↓
Controller
↓
JSON Response
这种方式很诱人,因为前端未来可能需要更多字段,不用重新设计接口,还能减少重复代码。
但问题是:数据库模型本来就不是给外部看的。数据库实体描述的是"系统保存什么",而 API 响应应该描述"当前调用者需要知道什么"。这两个目标完全不同。
一个 User 对象可能包含:
- 密码 Hash;
- 密码重置信息;
- 登录失败次数;
- 安全时间戳;
- 第三方认证 ID;
- 管理员标记;
- 内部备注。
员工对象可能包含:
- 工资信息;
- 身份编号;
- 私人联系方式;
- 纪律记录。
订单对象可能包含:
- 风控评分;
- 支付渠道信息;
- 内部利润;
- 客服备注。
这些字段即使前端页面不显示,也已经发送到了浏览器。用户可以通过浏览器 Network 面板、前端状态、直接调用 API 等方式查看这些数据。
很多开发者会认为:"页面没展示,所以没泄露。"这是错误的。数据一旦发送给客户端,就已经离开服务器控制范围。
更安全的方式是创建专门的数据响应模型。不要返回所有字段然后排除危险字段,而应该只返回明确允许的数据。例如:
function toPublicProfile(user: User) {
return {
id: user.id,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
joinedAt: user.createdAt
}
}
这种方式看起来代码更多,但它有一个巨大优势:每增加一个字段,都必须经过一次明确审核。
2. 把秘密放进前端代码
这是很多现代前端项目容易犯的问题。开发者创建 .env 文件,然后写:
API_SECRET=xxxx
感觉没有提交 Git,应该安全。但问题是:如果这个变量被前端代码使用,最终它一定会进入 JavaScript Bundle。
浏览器运行代码,意味着用户必须拿到代码。用户拿到代码,就意味着里面所有内容都是公开的。
很多人尝试压缩代码、混淆变量名、隐藏调用位置,这些都不能保护真正的秘密。攻击者可以分析静态文件、查看 Network 请求、调试运行环境、读取内存。
典型错误:第三方服务提供公开浏览器 Key 和服务器私密 Key,结果开发者把服务器 Key 放进前端,因为"这样调用更方便"。
最终结果:应用正常运行,直到有人提取 Key,然后消耗你的额度、调用你的接口、执行你的权限操作。
正确方式:敏感权限永远留在服务器。前端只能请求它被允许执行的操作。流程应该是:
浏览器
↓
后端接口
↓
权限验证
↓
第三方服务
↓
返回结果
前端可以请求权限,但不能拥有权限本身。
3. 把敏感信息放进 URL
URL 非常方便。它可以复制、分享、收藏、跳转。但也正因为如此,它会出现在很多地方。
一个 URL 可能进入:
- 浏览器历史;
- 代理日志;
- 负载均衡日志;
- 分析系统;
- 截图;
- 客服聊天;
- 书签;
- Referer 请求头。
所以这些内容不应该放入 URL:
- 密码;
- Token;
- 恢复码;
- 私密搜索内容;
- 个人身份信息;
- 私有文件数据。
例如密码重置链接,它必须包含临时 Token。但这个 Token 应该短时间有效、只能使用一次、难以预测、使用后立即失效。
登录流程中,不要长期把认证 Token 放在 Query 参数里。如果 OAuth 流程返回临时授权码,应该立即交换并清理 URL。
很多团队只关注 HTTPS 是否安全,但 HTTPS 只保护传输过程。URL 最终仍然会被浏览器、服务器、监控系统、第三方工具保存。
URL 天生就是用来传播的,不要把秘密放在那里。
4. 在日志和监控系统中记录秘密
日志系统经常会成为隐藏的数据仓库。
开发者遇到问题就记录完整请求,接口异常就保存全部 Headers,错误发生就上传完整用户状态。这些信息确实方便调试,但可能包含密码、Cookie、Token、银行卡信息、私人消息、医疗信息、身份证明。
很多公司的数据库权限非常严格,但日志可能更多人可以访问、被导入第三方平台、复制到测试环境、保存数月甚至几年。于是,日志系统变成了另一个生产数据副本。
自动化监控也可能泄露。例如 HTTP 追踪记录 Headers,数据库追踪记录 SQL 参数,错误平台收集页面状态、用户输入、Session 录像。
正确方式:数据离开应用前就进行脱敏,不要指望之后靠过滤解决。
日志应该记录必要上下文,而不是所有信息。例如支付失败时,记录交易 ID、错误类型、请求编号,而不要记录完整支付数据。
"以后可能需要,所以全部保存。"这不是监控,这是无控制的数据收集。
5. 文件导出制造新的数据副本
很多应用都会生成文件:客户列表、财务报表、账号备份、调查报告、CSV、Excel、PDF。
问题是:文件一旦生成,它就拥有新的生命周期。它可能上传到云存储、发送邮件、进入客服系统、保存在共享电脑、长期存在临时目录。
很多云存储泄露事件,本质都是文件权限管理失败。例如公开 Bucket、长期有效下载链接、过大的访问权限。
另外,导出功能也容易包含过多字段。因为开发者通常这样写:加载完整对象,导出所有字段。于是密码 Hash、内部备注、隐藏状态、私人信息全部进入 Excel。
安全导出应该明确字段、限制权限、设置过期时间、控制生命周期。
导出文件不是原数据,它是一个新的数据副本。新的副本,需要新的安全策略。
6. 把用户数据发送给第三方分析工具
数据分析通常从合理需求开始:哪些按钮被点击?用户在哪里退出?哪个页面体验不好?
然后不断增加字段:邮箱、公司名称、搜索内容、表单数据、用户 ID。最终,分析事件变成了一条新的数据流。
类似问题还存在于客服插件、Session Replay、聊天工具、错误监控、A/B 测试平台。
尤其是录屏分析工具,它可能捕获输入内容、账户信息、私人页面。
第三方工具不一定不安全,很多都有合规认证、权限控制、安全配置。但应用必须明确什么数据允许发送。
更好的方式:使用内部 ID,不要发送个人信息,不要自动收集用户输入,敏感页面减少埋点。
每一次 Analytics 调用,本质上都是一次数据披露决定。
7. 保存敏感数据太久
很多泄露发生的原因不是数据被偷,而是数据从来没有删除。
例如:
- 身份验证文件,审核完成后继续保存。
- 旧 Token,多年留在数据库。
- 删除账号,只是隐藏,完整数据仍然存在。
- 备份,保存多年。
长期保存看起来更安全,因为方便客服、方便调查、方便恢复。但问题是:保存越多,未来泄露影响越大。
一个系统不存在的数据永远不会泄露。数据最小化不仅是隐私原则,也是安全策略。
删除不能只删除数据库,还需要考虑搜索索引、缓存、数据仓库、对象存储、备份。
很多敏感数据一直存在,不是因为有人故意保留,而是没有人负责它的完整生命周期。
数据泄露通常发生在攻击之前
很多人想象数据泄露是黑客攻击数据库,直接拿走数据。但现实中,数据可能早就在这些地方存在:API 响应、前端 Bundle、URL、日志、导出文件、云存储、分析平台、备份系统。
攻击者甚至不需要进入核心数据库。
所以,加密和权限控制当然重要,但还不够。真正安全的数据系统需要:
- 减少副本数量。
- 限制返回范围。
- 避免客户端保存秘密。
- 控制日志内容。
- 限制第三方收集。
- 及时删除无价值数据。
最值得检查的方法:跟踪一个敏感字段,问自己——它在哪里产生?保存在哪里?哪些 API 返回它?是否进入日志?是否进入前端?是否发送给第三方?是否进入备份?什么时候删除?
答案通常比数据库结构复杂得多。
真正安全的系统,不是因为没有数据,而是因为每一次数据流动都经过明确设计。最容易避免的数据泄露,往往不是高级攻击造成的,而是普通代码复制了一份数据,然后忘记保护它。
如果你想深入讨论这类安全实践,欢迎到 云栈社区 与其他开发者一起交流。