前端敏感信息防护实战技巧与避坑指南

公孙诗雯 安全 阅读 1,462
赞 16 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

前端处理敏感信息这事儿,说白了就是别把不该暴露的东西扔到浏览器里。我之前做过一个后台管理系统,登录后用户的权限列表、个人信息全在前端存着,当时图省事直接挂在 window.userInfo 上,结果测试一跑安全扫描,直接挂红——人家用 DevTools 几下就改了权限,进了管理员页面。从那以后我就彻底清醒了:前端没有绝对安全的地方,但能做的防护一点都不能少。

前端敏感信息防护实战技巧与避坑指南

我现在处理敏感数据的核心原则就一条:**绝不存储,能不传就不传,非得传就加密再传**。比如用户身份证号、手机号这种,页面上显示可以,但 JS 里拿到的是加密串,展示靠服务端返回脱敏后的值,或者前端用 placeholder 加映射机制动态渲染。

举个实际例子:我们有个订单页要显示用户手机号,但不能让爬虫或恶意用户批量抓取。我的做法是,接口返回的手机号字段本身就是星号脱敏过的,比如 138****1234,需要查看完整号码时,走一个独立的鉴权接口,带 token 和操作日志记录。关键代码如下:

// 获取脱敏数据
async function getOrders() {
  const res = await fetch('/api/orders', {
    headers: { Authorization: Bearer ${getToken()} }
  });
  return res.json();
}

// 触发查看敏感信息(需二次验证)
async function revealPhone(orderId) {
  // 弹出验证码输入框或其他验证方式
  const code = prompt('请输入验证码');
  if (!code) return;

  const res = await fetch(/api/order/${orderId}/phone, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: Bearer ${getToken()}
    },
    body: JSON.stringify({ verifyCode: code })
  });

  if (res.ok) {
    const { phone } = await res.json();
    // 更新视图,仅临时显示
    document.getElementById(phone-${orderId}).textContent = phone;
  } else {
    alert('验证失败');
  }
}

这个设计的好处是:敏感信息不在主列表接口中暴露,每次查看都有审计日志,而且前端不会长期持有明文。就算有人翻源码,也找不到“全局变量存手机号”的漏洞点。

这几种错误写法,别再踩坑了

我见过太多项目在这上面栽跟头,下面这几个反面案例,都是我在接手老项目时血泪总结出来的。

  • 把 Token 存 localStorage,还明文挂在请求头里:这是最常见也最危险的操作。虽然方便调试,但 XSS 一打一个准。攻击者注入一段脚本,localStorage.getItem('token') 拿走,直接伪造请求。我之前维护的一个 H5 项目就是因为这个被刷了几千条虚假订单。
  • 用注释标记敏感字段:比如写 // 注意!这里包含身份证信息,结果构建没去掉注释,生产环境源码里还能搜到。你以为是提醒队友,其实是提醒黑客从哪下手。
  • 在前端做“假隐藏”:比如把敏感字段设为 display: none 或加 class 隐藏,但 DOM 里照样存在。稍微懂点前端的人 F12 就能看到。更离谱的是有人用 JS 把值赋给不可见 input,以为这样安全,其实 Network 面板一眼就能看到响应体。
  • 配置文件里写测试密钥:开发时为了快,直接在 config.js 里写个测试 API Key,结果忘了删,git commit 提交上去了。GitHub 搜索一下 jztheme.com/api + “key”,一堆这样的公开仓库,真不是开玩笑。

还有一个我亲身经历的坑:有次我们用 Webpack DefinePlugin 注入环境变量,把某些调试开关打开,其中包含了 mock 数据路径和模拟用户 ID。上线后没关,导致外部人员通过翻源码找到了内部测试账号,直接登录体验系统搞了一波数据篡改。后来我们改成构建时根据 NODE_ENV 动态生成配置,敏感项全由 CI/CD 注入,本地只留默认空值。

实际项目中的坑

有些问题不到真实场景真发现不了。比如我们有个导出功能,前端拼 URL 带参数跳转,形如:

window.location.href = /export?userId=${userId}&type=full;

看起来没问题,但问题是这个链接会被记录在服务器 access log 里,而日志系统没做敏感字段过滤,运维查问题时随手一翻就看到了用户 ID。后来我们改成 POST 导出,用 Blob 下载,URL 不带参数,同时服务端对导出行为做权限校验和日志脱敏。

还有一次更尴尬:我们在 console.log 里打印了用户余额和积分详情,方便测试定位问题。结果构建忘记清掉 console,生产环境 sourcemap 又开着,别人反向还原代码一看,连计算逻辑都清楚。现在我们统一上了 babel 插件 babel-plugin-transform-remove-console,只保留 error 级别的输出。

另外提一句,很多人忽略 Source Map 的风险。你打包上传的 js.map 文件如果可访问,别人能还原你的原始代码结构,找到所有变量命名、API 路径、加密逻辑的位置。建议做法是在 CI 流程中把 source map 单独上传到监控平台(比如 Sentry),而不是放在公网可访问路径下。

再说个容易被忽视的点:**第三方 SDK**。我们集成过某个广告统计 SDK,文档说只收集设备信息,结果抓包发现它偷偷读了页面上的手机号元素(通过 class 名匹配)。后来我们对敏感区域加了 data-no-track 属性,并在全局 CSS 里设置 [data-no-track] { visibility: hidden; },既不影响展示,又能防采集。

加密不是万能的,但不用更不行

我知道有人会说:前端加密没意义,反正密钥也得暴露。这话没错,但不能因此就不做任何处理。至少可以增加攻击成本。

我现在对敏感字段的做法是:用 AES 在前端做一层临时加密,密钥由后端短期签发(类似 JWT 的思路),有效期几分钟。比如用户提交表单前,敏感字段先加密再发:

// 简化示例,实际应使用 Web Crypto API
async function encryptField(value, aesKey) {
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(aesKey),
    { name: 'AES-GCM' },
    false,
    ['encrypt']
  );
  const encrypted = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv: crypto.getRandomValues(new Uint8Array(12)) },
    key,
    encoder.encode(value)
  );
  return btoa(String.fromCharCode(...new Uint8Array(encrypted)));
}

// 使用
const encryptedId = await encryptField(idCardNumber, temporaryAesKey);
await fetch('/api/submit', {
  method: 'POST',
  body: JSON.stringify({ idCard: encryptedId })
});

虽然最终密钥还是可能被截获,但这至少防止了静态分析直接看到明文传输。而且结合 HTTPS + 请求频率限制,自动化爬虫很难批量破解。

最后的小技巧

几个实用的小建议,不一定完美,但有效:

  • 敏感字段的变量名别叫 passwordidCard 这种直白的,混淆一下,比如叫 field_x9k2,增加逆向难度。
  • DOM 中不要用 data- 属性存敏感值,比如 data-user-id="123",网络爬虫专门扫这个。
  • 考虑用 WeakMap 临时存关联数据,避免全局对象污染,且能自动释放内存。
  • 定期跑 npm auditwebpack-bundle-analyzer,看看有没有意外引入高危依赖或泄露信息。

以上是我踩坑后的总结,希望对你有帮助。这个领域没有一劳永逸的方案,只能不断加固。有更好的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论