XSS 绕过分析:一次循环与两次循环的区别

目录

代码分析

代码流程:

一次循环的问题

原因分析:删除顺序导致遗漏

两次循环修复方案

两种绕过方式

[绕过方法 1:DOM破环](#绕过方法 1:DOM破环)

[绕过方法 2:SVG XSS(双 SVG 绕过)](#绕过方法 2:SVG XSS(双 SVG 绕过))

[1. 为什么 "一个SVG注定失败,两个SVG直接成功"?](#1. 为什么 "一个SVG注定失败,两个SVG直接成功"?)

[2. 为什么属性被删除后SVG仍能触发XSS?](#2. 为什么属性被删除后SVG仍能触发XSS?)

复现对比实验

场景1:单SVG(失败)

场景2:双SVG(成功)

防御建议:


代码分析

复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form id=x>
        <input id=attributes>
        <input id=attributes>
    </form>
</body>
<script>
    console.info(x.attributes);
    const data = decodeURIComponent(location.hash.substr(1));
    const root = document.createElement('div');
    root.innerHTML = data;
 
    // 这里模拟了XSS过滤的过程,方法是移除所有属性
    for (let el of root.querySelectorAll('*')) {
        for (let attr of el.attributes) {
            el.removeAttribute(attr.name);
        }
    }
    document.body.appendChild(root); 
</script>
</html>

代码流程:

  1. 通过 decodeURIComponent(location.hash.substr(1)) 获取 URL # 号后面的内容。
  2. 创建一个 div,并将获取的内容赋值给 div.innerHTML
  3. 遍历 div 内部的所有子元素,并移除它们的所有属性。
  4. div 添加到 document.body

一次循环的问题

测试 Payload:

复制代码
<img src=1 οnerrοr=alert(1)>

意外结果: src=1 被删除,但 onerror=alert(1) 仍然存在。

原因分析:删除顺序导致遗漏

  1. for (let attr of el.attributes) 遍历元素的属性。
  2. src 作为第一个属性被删除后,属性列表的索引发生变化。
  3. onerror 变成第一个属性,而 for 循环仍在进行,导致跳过了 onerror

绕过方法:

复制代码
<img test=aaa src=1 title=bbb οnerrοr=alert(1)>

通过增加 test=aaatitle=bbb 等属性,可以让 onerror 处于不会被跳过的位置,从而绕过一次循环的属性删除。


两次循环修复方案

改进代码:

复制代码
<script>
    const data = decodeURIComponent(location.hash.substr(1));
    const root = document.createElement('div');
    root.innerHTML = data;
   
    // 这里模拟了XSS过滤的过程,方法是移除所有属性,sanitizer
    for (let el of root.querySelectorAll('*')) {
     let attrs = [];
     for (let attr of el.attributes) {
      attrs.push(attr.name);
     }
     for (let name of attrs) {
      el.removeAttribute(name);
     }
    }    
     document.body.appendChild(root); 
</script>

两次循环的改进点:

  • 第一次循环 先收集所有属性的名称。
  • 第二次循环 再删除这些属性,避免索引错位问题。

两种绕过方式

绕过方法 1:DOM破环

原理:onfocus 事件先触发,移除 onfocus 后,仍然可以执行 alert(1)

复制代码
<form%20 tabindex=1 οnfοcus="alert(1);this.removeAttribute('onfocus');" autofocus="true">
  <input name=attributes>
  <input name=attributes>
</form>

过程:

  1. <form> 被解析时,tabindex=1 使其获得焦点。
  2. autofocus="true" 使 <form> 立即聚焦。
  3. onfocus 触发 alert(1),并删除自身的 onfocus
  4. 代码执行时,属性删除已经无法影响 XSS 触发。

绕过方法 2:SVG XSS(双 SVG 绕过)

复制代码
<svg><svg/οnlοad=alert(1)>

1. 为什么 "一个SVG注定失败,两个SVG直接成功"?

关键原因:浏览器解析的时序差异

  • 单SVG场景

    python 复制代码
    <svg><image href="valid.png" onload="alert(1)"></svg>
    1. 当插入DOM时,浏览器会立即解析SVG并开始加载<image>href资源。

    2. 资源加载是异步的onload事件会在资源加载完成后触发。

    3. 此时代码中的属性删除循环(el.removeAttribute)可能已经执行完毕,移除了onload属性。

    4. 事件未被绑定:因为属性在事件触发前已被删除,XSS无法触发。

  • 双SVG场景

    php 复制代码
    <svg></svg> <!-- 第一个SVG -->
    <svg><image href="valid.png" onload="alert(1)"></svg> <!-- 第二个SVG -->
    1. 浏览器解析第一个SVG时,会同步处理其子元素。

    2. 当解析第二个SVG时,浏览器可能已经进入异步任务队列

    3. 事件触发时机提前 :第二个SVG的<image>onload事件可能在属性删除循环完成前触发。

    4. XSS成功:事件回调在属性被删除前已绑定到DOM元素。

本质

  • 多个SVG可能改变浏览器的事件循环时序,导致属性删除和事件触发的竞争条件(Race Condition)。

2. 为什么属性被删除后SVG仍能触发XSS?

关键原因:事件触发与属性删除的异步性

  • 示例代码

    php 复制代码
    <svg><image onload="alert(1)"></svg>
    1. 插入DOM阶段 :浏览器解析SVG并立即触发<image>onload事件。

    2. 事件触发是异步的 :即使后续通过removeAttribute删除了onload属性,但事件回调已经在事件队列中等待执行。

    3. 执行顺序

      php 复制代码
      root.innerHTML = data;       // 插入SVG,触发onload(事件进入队列)
      deleteAllAttributes();       // 删除onload属性
      // 事件队列中的回调仍会执行!

技术细节

  • 事件绑定与属性无关onload="..."内联事件处理函数,浏览器在解析HTML时会直接将其转换为事件监听器,而不是依赖属性存在与否。

  • 属性删除仅移除了HTML特性,但无法撤销已经注册的事件回调。


复现对比实验

场景1:单SVG(失败)
php 复制代码
// 插入内容:<svg><image onload="alert(1)"></svg>
root.innerHTML = data;
// 浏览器解析SVG,触发onload(事件进入队列)
deleteAllAttributes(); 
// 事件回调尝试执行时,发现image元素已无onload属性,可能被浏览器忽略
场景2:双SVG(成功)
php 复制代码
// 插入内容:<svg></svg><svg><image onload="alert(1)"></svg>
root.innerHTML = data;
// 第一个SVG解析完成(同步)
// 解析第二个SVG时,可能已进入异步队列
deleteAllAttributes(); 
// onload事件在删除前已绑定,回调执行成功

防御建议:

  1. 彻底禁用 innerHTML

    复制代码
    document.body.textContent = data;
  2. 使用 DOMPurify 进行过滤

    复制代码
    import DOMPurify from 'dompurify';
    root.innerHTML = DOMPurify.sanitize(data, { FORBID_ATTR: ['onload', 'onfocus'] });
  3. 隔离 SVG 解析

    复制代码
    const svgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svgContainer.innerHTML = data;
相关推荐
赵大仁几秒前
深入理解 Pinia:Vue 状态管理的革新与实践
前端·javascript·vue.js
小小小小宇3 分钟前
业务项目中使用自定义Webpack 插件
前端
小小小小宇31 分钟前
前端AST 节点类型
前端
小小小小宇1 小时前
业务项目中使用自定义eslint插件
前端
babicu1231 小时前
CSS Day07
java·前端·css
小小小小宇1 小时前
业务项目使用自定义babel插件
前端
前端码虫1 小时前
JS分支和循环
开发语言·前端·javascript
GISer_Jing1 小时前
MonitorSDK_性能监控(从Web Vital性能指标、PerformanceObserver API和具体代码实现)
开发语言·前端·javascript
余厌厌厌1 小时前
墨香阁小说阅读前端项目
前端
fanged2 小时前
Angularjs-Hello
前端·javascript·angular.js