
1. 前端安全到底在防什么前端安全这个话题很多人第一反应是“XSS 和 CSRF 嘛面试背过”。但真到了项目里你会发现攻击面远比这两个词宽得多。浏览器给了前端极大的能力——读写 DOM、发请求、存数据、调硬件、开新窗口——每一项能力被滥用都是一个安全问题。前端安全的本质是在“用户能完全控制自己浏览器”这个前提下尽可能保护用户数据、业务逻辑和系统资源不被恶意利用。这句话听起来有点绕我换个说法。后端安全的前提是“服务器是我的代码跑在我的机器上”所以可以做各种校验和隔离。但前端代码是明文发给用户的用户想改就改想绕过就绕过。所以前端安全的目标不是“让攻击者无法篡改”而是“即使攻击者篡改了前端也拿不到不该拿的东西、做不了不该做的事”。这个思维转变非常关键后面所有的防护手段都是围绕这个前提展开的。这篇文章适合谁看如果你是刚入行的前端想系统梳理一遍安全知识那这篇可以当作你的检查清单如果你是有几年经验的开发者但安全一直是“知道个大概”那这篇会帮你把零散的知识点串成体系如果你是团队的技术负责人想给项目做一次安全自查那文中的排查表和实操步骤可以直接拿去用。我先把结论摆出来前端安全问题大致可以分成四类——注入类XSS、模板注入、请求伪造类CSRF、点击劫持、数据泄露类敏感信息暴露、存储不安全、依赖与供应链类第三方库漏洞、CDN 劫持。这四类的成因、危害和防护手段各不相同但有一个共同点大部分问题不是技术难题而是意识问题。很多漏洞的出现纯粹是因为开发者“没想到还能这样”。2. 注入类攻击XSS 及其变种2.1 XSS 的三种类型与真实危害XSS跨站脚本攻击常年霸占安全漏洞排行榜前列不是因为它难防而是因为它太容易出现了。只要页面把用户输入当 HTML 渲染就可能中招。XSS 分三种类型很多人背得出名字但说不清区别我用实际场景解释一下。存储型 XSS是最危险的一种。攻击者把恶意脚本提交到服务器比如评论区、用户昵称、商品评价服务器存进数据库其他用户访问页面时脚本被加载执行。这种攻击的可怕之处在于不需要诱导点击只要有人看这个页面就会中招。我之前见过一个案例某论坛的用户签名档没有过滤攻击者写了一段脚本所有看过他帖子的人 cookie 都被发到了攻击者服务器包括管理员的。结果整个论坛被拖库。反射型 XSS需要诱导用户点击一个特制链接。比如搜索页面把关键词直接显示在页面上“您搜索的 xxx 没有结果”。如果 xxx 是script.../script就会被执行。这种攻击通常配合钓鱼邮件或社交工程使用链接看起来是正常域名用户容易上当。DOM 型 XSS比较特殊恶意数据不经过服务器完全在前端 JS 里流转。比如document.getElementById(x).innerHTML location.hash.slice(1)攻击者构造#img srcx onerroralert(1)就能触发。这种漏洞在纯前端路由的应用里特别常见因为开发者觉得“数据没经过后端应该安全”恰恰相反。危害方面XSS 能做的事远超大多数人想象。窃取 cookie 只是基础操作还能读取页面上的敏感数据比如 CSRF token、发起任意请求以用户身份、篡改页面内容钓鱼、记录键盘输入、调用浏览器 API摄像头、麦克风、地理位置、甚至结合 WebSocket 做持久化控制。我见过最狠的一个案例攻击者通过 XSS 在页面里注入了一个假的登录框用户输入账号密码后直接发到攻击者服务器页面还正常跳转用户完全无感。2.2 防护 XSS 的核心原则与实操防护 XSS 的核心原则只有一条永远不要相信任何用户输入永远不要把它当代码执行。具体落地分几个层面。输出编码是第一道防线。根据数据放置的位置选择不同的编码方式HTML 内容区用 HTML 实体编码变lt;HTML 属性区用属性编码并始终加引号JS 变量区用 JS 编码URL 参数用 URL 编码。很多开发者只做了一层 HTML 编码就以为万事大吉结果数据放进onclick属性里照样被突破。输入过滤是辅助手段。为什么说辅助因为输入过滤很容易被绕过大小写、编码、特殊字符而且过滤太严会影响正常业务。我的建议是对明确的危险标签script、iframe、object做黑名单过滤但绝不依赖它作为唯一防线。使用成熟的净化库。如果业务确实需要渲染富文本比如博客编辑器不要自己写过滤规则用 DOMPurify 这类经过大量测试的库。我试过自己写正则过滤结果被一个img srcx onerroralert(1)就绕过了因为正则没考虑到onerror这种事件属性。CSP 是最后一道保险。内容安全策略Content Security Policy通过 HTTP 头告诉浏览器“只允许加载哪些来源的脚本”。即使攻击者成功注入了脚本CSP 也能阻止它执行。配置示例Content-Security-Policy: default-src self; script-src self https://trusted-cdn.com; style-src self unsafe-inline这条策略的意思是默认只允许同源资源脚本只允许同源和指定 CDN样式允许同源和内联。注意unsafe-inline会削弱防护效果但很多老项目离不开内联样式需要权衡。注意CSP 配置错误会导致页面功能异常建议先用Content-Security-Policy-Report-Only模式观察一段时间收集违规报告后再正式启用。2.3 模板注入与危险 API 清单除了 XSS前端还有一类容易被忽视的注入问题模板注入。很多前端框架支持在模板里执行表达式如果用户输入被直接拼进模板就可能执行任意代码。比如某些模板引擎的{{ }}语法如果用户输入{{constructor.constructor(alert(1))()}}在某些配置下会被执行。防护模板注入的关键是不要把用户输入当模板编译。如果需要动态渲染用框架提供的安全 API比如 Vue 的v-text、React 的{}插值它们默认会做转义。下面这份危险 API 清单建议每个前端都存一份代码审查时对照检查危险 API风险安全替代方案innerHTML直接解析 HTMLXSS 重灾区textContent、innerTextouterHTML同上还会替换自身用 DOM API 创建元素document.write可写入任意 HTML用appendChildeval执行任意 JS 代码JSON.parse、函数映射表new Function同上避免动态生成函数setTimeout(string)字符串会被当代码执行传函数而非字符串setInterval(string)同上传函数而非字符串location.href 用户输入可能执行javascript:协议校验协议白名单element.setAttribute(onclick, 用户输入)事件属性注入用addEventListener这张表里的每一条我都见过真实项目踩坑。特别是setTimeout传字符串这种写法很多老代码里还有迁移时一定要改掉。3. 请求伪造与界面劫持3.1 CSRF 的攻击链条与防御组合拳CSRF跨站请求伪造的思路和 XSS 不同它不窃取用户数据而是冒用用户身份发起请求。攻击者不需要知道用户的密码只需要诱导用户点击一个链接或访问一个页面浏览器会自动带上该站点的 cookie服务器就以为是用户本人操作。攻击链条通常是这样用户登录了银行网站 Acookie 还在有效期内用户又访问了恶意网站 BB 的页面里藏了一个img srchttps://bank-a.com/transfer?toattackeramount10000浏览器加载这个图片时自动带上 A 的 cookie 发起请求A 的服务器验证 cookie 有效执行转账。整个过程用户毫无感知。防御 CSRF 有三板斧组合使用效果最好。第一板斧CSRF Token。服务器生成一个随机 token存在 session 里同时嵌入页面表单。提交时服务器比对 token 是否匹配。攻击者的页面无法读取这个 token受同源策略保护所以伪造的请求会失败。这是最经典的防御手段但要注意token 不能放在 URL 里会泄露不能放在 cookie 里攻击者可以利用要放在表单隐藏字段或自定义请求头里。第二板斧SameSite Cookie。设置 cookie 的SameSite属性为Strict或Lax浏览器就不会在跨站请求中发送这个 cookie。Strict最安全但会影响从外部链接跳转过来的登录状态Lax是折中方案允许顶级导航的 GET 请求携带 cookie但 POST 和 iframe 不带。现在主流浏览器默认就是Lax但显式设置更稳妥。Set-Cookie: sessionIdabc123; SameSiteLax; Secure; HttpOnly第三板斧验证请求来源。检查Origin和Referer头只允许同源请求。这个方法简单有效但要注意Referer可能被隐私设置屏蔽所以Origin更可靠。实操心得很多团队只做了 Token 就以为万事大吉结果登录接口、注销接口忘了加照样被 CSRF。我的经验是所有会改变状态的接口POST/PUT/DELETE都必须验证包括那些看起来“不重要”的接口。3.2 点击劫持与 iframe 防护点击劫持Clickjacking是一种视觉欺骗攻击。攻击者把目标网站用 iframe 嵌到自己的页面里设置透明度为 0上面盖一个诱导性的按钮比如“点击领取奖励”。用户以为点的是按钮实际点的是 iframe 里的“确认转账”。防御点击劫持主要靠 HTTP 头X-Frame-Options: DENY或者用 CSP 的frame-ancestors指令更灵活Content-Security-Policy: frame-ancestors selfX-Frame-Options有三个值DENY完全禁止嵌入SAMEORIGIN只允许同源嵌入ALLOW-FROM uri允许指定来源但兼容性差不推荐。frame-ancestors是更现代的方案支持多个来源和通配符。另外前端也可以用 JS 做“帧爆破”frame busting检测自己是否被嵌入如果是就跳转出去。但这种方案容易被绕过攻击者可以加sandbox属性禁止脚本所以只能作为辅助。3.3 开放重定向与 URL 跳转陷阱开放重定向是指网站接受用户提供的 URL 参数并跳转过去攻击者可以构造一个看起来是合法域名的链接实际跳转到钓鱼网站。比如https://trusted-site.com/redirect?urlhttps://evil-site.com用户看到域名是 trusted-site.com 就放心点了结果到了钓鱼站。防护方法很简单跳转目标用白名单校验或者只允许相对路径。如果业务需要跳转到外部用一个映射表比如?targethome对应首页而不是直接传 URL。// 不安全的写法 const target new URLSearchParams(location.search).get(url); location.href target; // 安全的写法 const allowedHosts [trusted-site.com, partner-site.com]; const target new URLSearchParams(location.search).get(url); const targetUrl new URL(target); if (allowedHosts.includes(targetUrl.hostname)) { location.href targetUrl.href; } else { location.href /; }这个坑我在实际项目里踩过。当时觉得“跳转嘛能有什么问题”结果安全扫描报了一个高危。后来加了白名单才过。4. 数据泄露与存储安全4.1 敏感信息暴露的常见场景前端代码是公开的这句话说起来简单但很多开发者就是记不住。我见过太多项目把 API 密钥、数据库连接串、内部接口地址直接写在前端代码里以为压缩混淆就看不出来了。实际上打开浏览器开发者工具或者用抓包工具这些信息一览无余。常见的敏感信息暴露场景包括硬编码密钥把第三方服务的 API Key 写在 JS 里攻击者拿去刷你的额度。注释泄露开发时的注释没删干净暴露了内部接口、测试账号、业务逻辑。Source Map 暴露生产环境没关 Source Map攻击者可以直接看到源码结构。错误信息泄露接口报错时把堆栈信息返回给前端暴露了框架版本、文件路径、数据库结构。接口返回过多数据后端把用户完整信息包括手机号、身份证号返回给前端前端只显示了一部分但数据都在响应里。防护的核心原则是前端只拿必要的数据敏感操作全部在后端完成。API 密钥这类东西绝对不能出现在前端代码里必须通过后端代理调用。Source Map 在生产环境要关闭或上传到错误监控平台不要公开发布。4.2 本地存储的安全选择前端存储数据的地方有好几个cookie、localStorage、sessionStorage、IndexedDB、Cache API。它们的安全特性各不相同选错了就是给自己挖坑。存储方式容量有效期是否随请求发送XSS 可读适用场景Cookie4KB可设置是是除非 HttpOnly会话标识localStorage5-10MB永久否是非敏感配置sessionStorage5-10MB标签页关闭否是临时表单数据IndexedDB较大永久否是大量结构化数据Cache API较大可控否是离线资源缓存关键点除了 HttpOnly 的 cookie其他存储方式都能被 JS 读取所以都不能存敏感信息。token 存哪里是个经典问题。我的建议是如果后端支持用 HttpOnly Secure SameSite 的 cookie 存 session前端 JS 读不到XSS 也偷不走。如果必须用 token存在内存里变量比存在 localStorage 安全但刷新页面会丢需要配合 refresh token 机制。注意localStorage 是永久存储用户不手动清理就一直在。如果存了敏感数据用户换电脑或共享设备时就会泄露。我见过一个项目把用户身份证号存在 localStorage 里结果在网吧环境下被下一个用户看到了。4.3 前端加密的误区与正确用法很多开发者觉得“前端加密一下再传输就安全了”这个想法有道理但不完整。前端加密能防什么能防简单的抓包窥探比如 HTTP 明文传输。不能防什么不能防中间人攻击攻击者可以替换你的加密脚本不能防 XSS脚本能拿到加密前的数据不能替代 HTTPS。正确的做法是传输层用 HTTPS这是底线。HTTPS 已经解决了传输过程中的窃听和篡改问题。前端加密只在特定场景下有意义比如端到端加密服务器也解不开、密码在发送前做一次哈希防止日志记录明文密码。但要注意前端哈希不能替代后端哈希后端仍然要用 bcrypt 这类慢哈希再处理一次。我见过最离谱的案例是某项目自己实现了一套 RSA 加密公钥硬编码在前端私钥也在前端因为要解密返回数据。这等于没加密攻击者直接读源码就能拿到私钥。所以不要自己发明加密方案用标准协议和成熟库。5. 依赖与供应链安全5.1 第三方库漏洞的排查与修复现代前端项目动辄几百个 npm 依赖你写的代码可能只有几千行但依赖的代码有几十万行。这些依赖里只要有一个存在漏洞你的整个应用就暴露了。排查依赖漏洞的标准工具是npm audit和yarn audit。运行后会列出所有已知漏洞按严重程度分级。npm audit npm audit fix npm audit fix --forcenpm audit fix会自动升级到兼容的安全版本--force会强制升级到最新版可能破坏兼容性慎用。如果自动修复搞不定就需要手动排查找到漏洞所在的包看是直接依赖还是间接依赖然后决定是升级、替换还是打补丁。我个人的经验是不要等到出事才查把npm audit加到 CI 流程里每次提交都跑一遍有高危漏洞就阻断合并。另外定期比如每月做一次全量依赖更新不要攒着攒久了升级就是灾难。5.2 供应链攻击的防范策略供应链攻击是指攻击者不直接攻击你的应用而是攻击你依赖的第三方库或服务。常见手法包括在 npm 包名上做文章typosquatting比如把lodash写成1odash、劫持维护者账号发布恶意版本、在 CDN 上篡改文件。防范策略有几个层面锁定依赖版本。用package-lock.json或yarn.lock锁定版本确保每次安装的都是同一份代码。不要用^或~这种模糊版本号生产环境要精确锁定。校验文件完整性。用 SRISubresource Integrity给 CDN 资源加哈希校验script srchttps://cdn.example.com/lib.js integritysha384-xxxxx crossoriginanonymous/script如果 CDN 上的文件被篡改哈希对不上浏览器就拒绝执行。这个手段对防御 CDN 劫持特别有效。减少依赖数量。每多一个依赖就多一个攻击面。能用原生 API 实现的就不要引库。我见过一个项目为了做个日期格式化引了 moment.js几百 KB其实Intl.DateTimeFormat就够了。审查依赖来源。引入新依赖前看看它的下载量、维护活跃度、issue 情况。一个半年没更新、issue 一堆没人管的库慎用。5.3 构建与部署环节的安全检查构建和部署环节也容易出安全问题。比如环境变量泄露把生产环境的密钥写进.env文件结果被提交到了 Git 仓库。解决方案.env加入.gitignore用 CI/CD 的密钥管理功能注入。构建产物包含源码Source Map 没关或者构建配置把源码也打包进去了。解决方案生产构建关闭 Source Map检查产物内容。部署目录可浏览服务器没配好用户可以直接访问目录列表看到所有文件。解决方案关闭目录浏览配置正确的默认文件。HTTP 头缺失没有设置安全相关的响应头。建议至少配置以下头Strict-Transport-Security: max-age31536000; includeSubDomains X-Content-Type-Options: nosniff X-Frame-Options: DENY Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: camera(), microphone(), geolocation()这些头的作用分别是强制 HTTPS、禁止 MIME 类型嗅探、禁止 iframe 嵌入、控制 Referer 发送策略、限制浏览器 API 权限。配置起来不麻烦但能挡掉一大批低级攻击。6. 常见问题排查与实战避坑6.1 安全问题速查表实际工作中遇到安全问题时往往不知道从哪下手。我整理了一份速查表按症状找原因症状可能原因排查方向页面弹出奇怪弹窗XSS 注入检查所有用户输入渲染点用户反馈“我没操作但数据变了”CSRF检查状态变更接口的 Token 验证搜索结果页显示异常反射型 XSS检查 URL 参数是否直接输出第三方接口额度异常消耗密钥泄露检查前端代码和网络请求用户登录状态莫名丢失Cookie 配置问题检查 SameSite、Secure、Domain页面被嵌入到其他网站点击劫持检查 X-Frame-Options构建产物异常大依赖冗余或源码泄露检查打包配置和 Source Map安全扫描报高危依赖漏洞运行 npm audit 并修复这张表不能覆盖所有情况但能帮你快速定位大方向。实际排查时浏览器开发者工具的 Network 面板和 Sources 面板是最常用的前者看请求后者看代码。6.2 代码审查中的安全检查清单代码审查是发现安全问题的最佳时机。我总结了几个必查点每次 review 代码时过一遍所有用户输入是否都做了转义或净化特别是innerHTML、v-html、dangerouslySetInnerHTML这些危险 API 的使用。所有状态变更接口是否有 CSRF 防护包括 POST、PUT、DELETE以及那些用 GET 实现的“删除”接口这本身就是个问题。敏感信息是否出现在前端代码或响应中搜索关键词password、secret、token、key、private。跳转链接是否做了白名单校验检查所有location.href、window.open、a href的动态赋值。第三方资源是否加了 SRI检查所有 CDN 引入的脚本和样式。安全响应头是否配置检查服务器配置或中间件。依赖是否有已知漏洞运行npm audit。错误处理是否泄露敏感信息检查catch块是否把原始错误返回给前端。这份清单看起来多但熟练之后几分钟就能过一遍。关键是养成习惯把安全检查变成肌肉记忆。6.3 被忽视的边缘场景与独家避坑技巧最后分享几个我在实际项目中踩过的坑都是常规文档里不会写的。坑一富文本编辑器的“安全模式”不一定安全。很多富文本编辑器号称有安全过滤但配置不当照样出问题。比如某编辑器的allowedTags配置漏了svg攻击者就能用svg onloadalert(1)绕过。我的做法是不管编辑器怎么吹输出时再过一遍 DOMPurify双重保险。坑二JSONP 是老代码里的定时炸弹。JSONP 通过script标签跨域请求数据但服务器返回的内容会被当 JS 执行。如果接口返回了用户可控的数据就是 XSS。现在基本被 CORS 取代了但老项目里还有。排查方法搜索代码里的callback和jsonp。坑三postMessage 没验证来源。跨窗口通信用的postMessage如果不验证event.origin任何页面都能给你发消息。我见过一个案例攻击者通过 iframe 嵌入目标页面然后 postMessage 发送伪造指令触发了敏感操作。正确写法window.addEventListener(message, (event) { if (event.origin ! https://trusted-site.com) return; // 处理消息 });坑四WebSocket 没有鉴权。很多人以为 WebSocket 建立连接时带了 cookie 就安全了但 WebSocket 不受同源策略限制任何页面都能连你的 WebSocket 服务。必须在连接建立时验证 token并且对每条消息做权限校验。坑五前端路由的“隐藏页面”并不隐藏。有些管理后台把管理员页面放在前端路由里靠“普通用户看不到入口”来防护。但路由配置是打包在 JS 里的攻击者直接输入 URL 就能访问。正确做法路由守卫 后端接口鉴权双管齐下。坑六自动填充带来的信息泄露。浏览器自动填充功能会把用户保存的地址、信用卡信息填到表单里。如果页面有隐藏字段或者被 XSS 注入了额外字段这些信息可能被读取。防护方法敏感表单加autocompleteoff但浏览器不一定听所以更可靠的是不在前端处理敏感信息。这些坑的共同点是它们都不是“技术难题”而是“没想到”。安全防护的难点从来不是技术而是思维方式的转变——从“功能能跑就行”到“假设有人会恶意使用”。我在实际项目中的体会是前端安全最有效的手段不是某个具体的技术而是建立检查习惯。每次写代码时多问一句“如果用户输入的是恶意内容会怎样”每次 review 时多看一眼“这里有没有验证”每次上线前多跑一遍“安全扫描过了没”。这些习惯坚持下来能挡掉 90% 以上的常见漏洞。剩下的 10%靠的是持续学习和关注安全社区的最新动态因为攻击手法也在不断进化。