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;
相关推荐
mldong4 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排5 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60615 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角6 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!6 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰7 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi8 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen9 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马9 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒10 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端