浏览器解析机制与XSS的15种编码绕过

目录

拆解浏览器三层解析器:从 HTML 到 URL 到 JavaScript


一、一个"正确"的过滤为什么失效了?

先看一个场景:

你写了这样一段代码:

php 复制代码
$input = $_GET['url'];
// 过滤尖括号,防止 HTML 注入
$safe = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
echo '<a href="' . $safe . '">点击</a>';

攻击者输入:

复制代码
javascript:alert(1)

htmlspecialchars<> 转义了,但尖括号在这里本来就没用------攻击者根本不需要 <>。他把 payload 改成这样:

html 复制代码
&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3a;&#x61;&#x6c;&#x65;&#x72;&#x74;(1)

弹窗了。

你的第一反应是:"我明明转义了 <>,为什么还能执行 JavaScript?"

答案是:浏览器不是"一次性"解析 HTML 的。它内部有三层解析器依次接力,每层有自己的编码规则和解码时机。攻击者可以利用层间编码差异,绕过你的单层防护。

这篇文章将用 15 个精心构造的案例,逐层拆解浏览器解析机制,让你彻底搞懂 XSS 编码绕过的底层原理。


二、核心模型:浏览器的三层解析架构

浏览器在渲染一个 HTML 页面时,会依次经过三个独立的解析器:

复制代码
原始 HTML 字符串
      │
      ▼
┌────────────────────────────────────────┐
│         第一层:HTML 解析器              
│                                        
│  · 解码 HTML 实体(&#x6a; → j)        
│  · 区分标签、属性、RCDATA、纯文本       
│  · 构建 DOM 树                          
│  · 遇到 </style> 或 </textarea> 切换状态 
└────────────────────────────────────────┘
      │
      ▼  (输出 DOM 树,属性值进入下一层)
      │
┌────────────────────────────────────────┐
│         第二层:URL 解析器               
│                                        
│  · 解析 href / src / action 等属性值   
│  · 识别协议(http: / https: / javascript:)
│  · 解码百分号编码(%6a → j)            
└────────────────────────────────────────┘
      │
      ▼  (识别为 javascript: 协议后)
      │
┌────────────────────────────────────────┐
│     第三层:JavaScript 解析器           
│                                        
│  · 解析 onclick / onerror 等事件属性    
│  · 解码 Unicode 转义(a → a)     
│  · 执行 JavaScript 代码                 
└────────────────────────────────────────┘
      │
      ▼
      alert(1)  ------ 弹窗!

三个核心原则(请反复读三遍):

  1. 解码是逐层进行的------上一层解码后的结果传给下一层,每层只能看到上一层处理完的输出
  2. 每层只认自己的编码 ------HTML 解析器不认识 \u,JS 解析器不认识 &#x;,URL 解析器不认识 HTML 实体
  3. 上下文决定解析路径 ------同一段输入放在 href vs onclick vs <script> 中,走完全不同的解析器组合

下面,我们用 15 个案例来验证这个模型。按照解析层分为 5 组讲解。


三、逐条详解 15 种绕过案例


🅰 组:HTML 实体编码的误解与真相

核心:HTML 实体编码在"数据状态"下只是字符,不会创建新标签

案例④ ------ <div> 中的 HTML 实体
html 复制代码
<div>&#60;img src=x onerror=alert(4)&#62;</div>

结果:❌ 不弹窗

解析过程:

  1. HTML 解析器进入 标签开始状态 ,看到 <div>
  2. 解析器进入 数据状态(Data State) ,开始处理 <div></div> 之间的文本
  3. 遇到 &#60;------这是 HTML 字符引用(Character Reference),解析器把它解码为 <
  4. 但是!此时解析器处于数据状态,不是标签开始状态(Tag Open State) 。解码后的 < 只是一个普通文本字符,不会触发"创建新标签"的逻辑
  5. 最终渲染为纯文本 <img src=x onerror=alert(4)>

💡 关键理解 :HTML 实体解码发生在任何状态 下,但"创建标签"只发生在标签开始状态 。要进入标签开始状态,必须有一个未被转义的 < 触发状态切换。一旦 < 被编码为 &#60;,解析器在文本状态解码它,看到的是字符 < 而不是状态机指令。


案例⑤ ------ <textarea> 中的 HTML 实体
html 复制代码
<textarea>&#60;script&#62;alert(5)&#60;/script&#62;</textarea>

结果:❌ 不弹窗

解析过程:

  1. HTML 解析器遇到 <textarea>,进入 RCDATA 状态
  2. RCDATA 状态的特点:允许 HTML 实体解码,但不允许解析新的标签
  3. &#60; 被解码为 <&#62; 被解码为 >,但解析器把它们都当作文本处理
  4. 在 RCDATA 状态下,只有 </textarea></title> 会被识别为标签结束标记

💡 RCDATA 是什么? HTML 规范定义了四种解析状态:普通元素(Normal)RCDATA 元素 (textarea、title)、原始文本元素(Raw Text) (script、style)、可替换元素(Void)。RCDATA 介于普通和原始之间:解码实体但不认标签。


案例⑥ ------ <textarea> 中的纯文本标签
html 复制代码
<textarea><script>alert(6)</script></textarea>

结果:❌ 不弹窗

解析过程:

  1. 同样是 RCDATA 状态
  2. <script> 不会被识别为标签开始------解析器只关心 </textarea>
  3. 整个 <script>alert(6)</script> 都是 textarea 的文本内容

💡 案例⑤和⑥的对比 :案例⑤至少进行了实体解码(&#60;<),案例⑥连实体解码都不需要------<script> 本身就不是一个合法的"状态切换指令"。这才是 RCDATA 的完整含义:仅解码实体,不解析标签。


案例⑨ ------ <script> 中的 HTML 实体
html 复制代码
<script>&#97;&#108;&#101;&#114;&#116&#40;&#57;&#41;&#59</script>

结果:❌ 不弹窗

解析过程:

  1. HTML 解析器遇到 <script>,进入 原始文本状态(Raw Text State)
  2. 在原始文本状态下,HTML 解析器不对内容做任何实体解码
  3. &#97; 就是一串普通字符 & # 9 7 ;,不会被解析成 a
  4. JS 解析器拿到的是 &#97;&#108;&#101;&#114;&#116(9);------这不是有效的 JS 语法,直接报错

💡 陷阱 :很多人以为"在 script 标签里编码就能绕过",但 HTML 规范明确规定:script 是原始文本元素,解析器在里面不解码任何实体。要执行 JS,必须使用 JS 层面的编码(下一组会讲)。


🅱 组:URL 协议识别的边界

核心:URL 解析器在判断协议类型时,看的是解码后的字符串开头是否匹配已知协议名。

案例① ------ URL 编码了整个协议前缀
html 复制代码
<a href="%6a%61%76%61%73%63%72%69%70%74:%61%6c%65%72%74%28%31%29">aaa</a>

结果:❌ 不执行

解析过程:

  1. HTML 解析器看到 href="%6a%61%76..."------这是一个普通的属性值字符串
  2. URL 解析器接手,检查字符串开头
  3. URL 解析器的协议识别规则:看第一个 : 之前的部分
  4. 第一个字符是 %,解码后是 j,但 URL 解析器是先识别协议名解码余下内容
  5. 它从左到右扫描,看到 %6a%61%76%61%73%63%72%69%70%74------这是一个没有注册的协议名(%6a%61... 不是 javascript 的字面形式)
  6. 最终认为这是一个无效协议,不执行 JS

💡 关键 :URL 解析器的"协议识别"发生在 URL 解码之前 。它只看原始字符串有没有以 javascript: 开头,而不是解码后有没有。所以 %6a%61%76%61%73%63%72%69%70%74 不等于 javascript


案例③ ------ URL 编码了冒号
html 复制代码
<a href="javascript%3aalert(3)">aaa</a>

结果:❌ 不执行

解析过程:

  1. URL 解析器扫描字符串,寻找 :
  2. 看到 javascript%3aalert(3)------在整个字符串中没有找到 : 字面字符
  3. 实际上 %3a 就是 : 的 URL 编码,但 URL 解析器的协议检测阶段不做 URL 解码
  4. 因为没有找到 :,URL 解析器认为这是一个无协议的相对路径 ,相当于 ./javascript%3aalert(3)
  5. 浏览器尝试把它当作相对 URL 处理,不会执行 JS

💡 对比 :案例①是编码了协议字母,案例③是编码了冒号------两条路都走不通。协议检测只看未解码的字符串。


案例② ------ HTML 实体 + URL 编码的双重绕过(✅ 成功的经典案例)
html 复制代码
<a href="&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;:%61%6c%65%72%74%28%32%29">aaa</a>

结果:✅ 弹窗!

这是最经典的多层解析绕过案例,我们来逐层拆解:

复制代码
原始输入:&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;:%61%6c%65%72%74(2)
                  │
                  │  第1层:HTML 解析器
                  │  href 属性值中的 HTML 实体被解码
                  │  &#x6a; → j, &#x61; → a, ... &#x74; → t
                  ▼
         javascript:%61%6c%65%72%74(2)
                  │
                  │  第2层:URL 解析器
                  │  扫描字符串开头 → 找到 "javascript:" 协议
                  │  协议识别成功!对剩下的部分做 URL 解码
                  │  %61 → a, %6c → l, %65 → e, %72 → r, %74 → t
                  ▼
              javascript:alert(2)
                  │
                  │  第3层:JavaScript 解析器
                  │  执行 alert(2)
                  ▼
                  弹窗!

为什么这个能成功而案例①和③不能?

案例 输入 HTML 解码后 URL 协议识别 结果
%6a%61%76...(协议被 URL 编码) 不变 不识别 %6a%61... 协议
javascript%3aalert(3)(冒号被 URL 编码) 不变 找不到 :
&#x6a;&#x61;...&#x74;:%61%6c... javascript:%61%6c... HTML 解码后 露出了 javascript:

案例②的成功密码在于 :攻击者把协议字母用 HTML 实体 编码,URL 协议部分用 URL 编码 。HTML 解析器先解码实体,露出了 javascript: 协议头,然后 URL 解析器正常识别协议并解码剩余部分。两层编码分别被两个解析器消化,互不冲突。


🅲 组:JavaScript 层的 Unicode 转义规则

核心:JS 支持 \uXXXX Unicode 转义,但只允许用于标识符字符(字母、数字、$、_),不能用于符号。

HTML 规范规定 script 是原始文本元素,不解码 HTML 实体。所以在 <script> 标签内,必须使用 JS 自己的转义机制------Unicode 转义。

案例⑩ ------ 字母的 Unicode 转义(✅ 成功)
html 复制代码
<script>alert(10);</script>

结果:✅ 弹窗!

解析过程:

  1. HTML 解析器在原始文本状态,不做任何解码,直接把字符串传给 JS 引擎
  2. JS 解析器开始词法分析
  3. a 被识别为 Unicode 转义序列,解析为 a
  4. lleerrtt
  5. alertalert
  6. (10) 是正常的函数调用
  7. 执行 alert(10)

💡 为什么? JS 规范允许在标识符(Identifier)中使用 Unicode 转义序列。alert 是一个标识符,所以 aa 是合法的。


案例⑪ ------ 符号也被 Unicode 转义(❌ 失败)
html 复制代码
<script>alert(11)</script>

结果:❌ 不执行

解析过程:

  1. JS 解析器尝试解析 (
  2. ( 解码为 (
  3. 但 JS 规范规定:Unicode 转义不能用于界定符(delimiter)和运算符(operator)
  4. 标识符中的 Unicode 转义和 字符串/正则中的 Unicode 转义 是两套不同规则
  5. ((左括号)不是一个合法的标识符字符,JS 词法分析器在遇到它时报错

💡 JS 中的性别歧视\u 转义在 JS 中"重男轻女"------它只允许出现在标识符名称 (字母、数字、$、_)中,不允许出现在符号 (括号、引号、运算符、分号等)中。所以 ((左括号)和 )(右括号)都会导致语法错误。


案例⑫ ------ 数字被 Unicode 转义
html 复制代码
<script>alert(12)</script>

结果:❌ 不执行

解析过程:

  1. a...rt 正常解析为 alert
  2. 左括号 ( 是普通字符,正常
  3. 1 解码为 12 解码为 2
  4. 但问题是:JS 解析器在解析标识符时,11 作为标识符是合法的(数字可以出现在标识符中)
  5. 然而这里的 12 是作为数值字面量出现的,不是标识符
  6. 在数值字面量中,JS 不支持 Unicode 转义
  7. 加上引号写成 '12' 没问题------因为在字符串中 \u 转义始终有效

💡 知识点 :JS 中有三种地方支持 \u 转义,每种规则不同:

  • 标识符中:仅字母、数字、$、_ 可转义
  • 字符串/模板字面量中:任意字符都可转义
  • 正则表达式 中:任意字符都可转义(在两个 / 之间)

案例⑭ ------ Unicode 换行符
html 复制代码
<script>alert('14
')</script>

结果:✅ 弹窗!

解析过程:

  1. 是换行符(Line Feed)的 Unicode 编码
  2. 在 JS 字符串中,\u 转义可以用于任意字符,包括控制字符
  3. alert('14 ') 等价于 alert('14\n')
  4. 弹窗内容为 14 后跟一个换行(可能看不见效果,但字符串内确实包含了一个换行符)

💡 与案例⑪的对比 :同样是 \u + 符号类字符,((括号)不行,但 (换行)可以。区别在于上下文------标识符 中只能转义标识符字符,字符串 中几乎可以转义任何字符。 出现在字符串字面量的内容中,所以合法。


案例⑧ ------ onclick 属性中的 Unicode 转义
html 复制代码
<button onclick="confirm('8');">Button</button>

结果:❌ 不弹窗

解析过程:

  1. HTML 解析器看到 onclick 属性值为 confirm('8');
  2. HTML 解析器不做特殊处理,直接将属性值传给 JS 解析器
  3. JS 解析器开始解析,看到 confirm('8');
  4. 单引号字符串 '8''------' 解码为 '(单引号)
  5. 但是! 这个 ' 是在字符串内部的 Unicode 转义
  6. 在 JS 字符串中,' 被解析为单引号字符 ',但它只是字符串内容的一部分,不会闭合外层字符串
  7. 也就是:'8'' → 字符串 8',字符串已经结束,后面的 ) 变成语法错误
  8. 等一下------不对,'8'' 这个字符串到底是什么?

这里要仔细分析 JS 的词法:

复制代码
confirm('8');
  • 开头的 ' 开始一个单引号字符串
  • 8 是字符串内容
  • ' 是 Unicode 转义序列,解析为 ' 字符
  • 然后遇到 )------字符串什么时候结束的?字符串没有结束!
  • 因为 ' 解析为字符 ',但它不会被当作字符串结束的定界符
  • 所以 JS 解析器继续搜索匹配的 ',直到字符串结束都没找到,报错

💡 关键理解\u 转义在字符串中只是"生成"一个字符,不具备语法功能。' 生成一个 ' 字符,但这个 '数据 ,不是语法标记 。就像 \' 在字符串中是一个转义的单引号字符一样,' 也是。它不会闭合外部字符串。

如果想用 Unicode 绕过 onclick 的引号过滤,出路在哪里?

答案是用 \u 转义函数名,而不是引号:

html 复制代码
<button onclick="alert(1)">Button</button>

这会执行 alert(1)------因为 alert 作为标识符可以被 Unicode 转义,而且不在字符串内部。


🅳 组:三重编码的终极套娃(案例 15🌟)

html 复制代码
<a href="&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3a;&#x25;&#x35;&#x63;&#x25;&#x37;&#x35;&#x25;&#x33;&#x30;&#x25;&#x33;&#x30;&#x25;&#x33;&#x36;&#x25;&#x33;&#x31;&#x25;&#x35;&#x63;&#x25;&#x37;&#x35;&#x25;&#x33;&#x30;&#x25;&#x33;&#x30;&#x25;&#x33;&#x36;&#x25;&#x36;&#x33;&#x25;&#x35;&#x63;&#x25;&#x37;&#x35;&#x25;&#x33;&#x30;&#x25;&#x33;&#x30;&#x25;&#x33;&#x36;&#x25;&#x33;&#x35;&#x25;&#x35;&#x63;&#x25;&#x37;&#x35;&#x25;&#x33;&#x30;&#x25;&#x33;&#x30;&#x25;&#x33;&#x37;&#x25;&#x33;&#x32;&#x25;&#x35;&#x63;&#x25;&#x37;&#x35;&#x25;&#x33;&#x30;&#x25;&#x33;&#x30;&#x25;&#x33;&#x37;&#x25;&#x33;&#x34;&#x28;&#x31;&#x35;&#x29;">aaa</a>

结果:✅ 弹窗!

这是全文中最复杂也最精彩的一个案例------它同时用到了三种编码,由三层解析器依次消化。

逐层拆解:

复制代码
原始输入:&#x6a;&#x61;&#x76;...&#x74;&#x3a;%35%63%75%33%30%30%36%31...
                  │
                  │  第1层:HTML 解析器
                  │  在 href 属性值中,HTML 实体被解码
                  │  &#x6a; → j, &#x61; → a, ..., &#x74; → t, &#x3a; → :
                  │  特别注意 &#x3a; 就是冒号 :
                  ▼
         javascript:%5c%75%33%30%30%36%31%5c%75%33%30%30%36%33...
                  │
                  │  第2层:URL 解析器
                  │  识别到 "javascript:" 协议 ✅
                  │  对协议后的内容做 URL 解码
                  │  %35 → 5, %63 → c, 等等
                  │  %5c → \  (反斜杠!)
                  │  %75 → u
                  │  组合起来就是 1, c, ...
                  ▼
         acert(15)
                  │
                  │  第3层:JavaScript 解析器
                  │  执行 Unicode 转义
                  │  a → a, c → c, ..., t → t
                  ▼
              alert(15)  →  弹窗!

编码嵌套关系:

复制代码
&#x25;&#x35;&#x63;  ← HTML 实体编码(第1层消化)
        ↓ 解码后
        %5c        ← URL 编码(对应 \)
                  ↓ URL解码后
                  \ ← 反斜杠字符
                    ↓ 和后面的 u 组合
                    a ← JS Unicode 转义(第3层消化)
                            ↓
                            a

攻击者的编码流程(反向):

复制代码
原始目标:alert(15)
     ↓ 第3层编码(JS Unocode 转义)
     acert(15)
     ↓ 第2层编码(URL 百分号编码)
     %5c%75%30%30%36%31%5c%75...
     ↓ 第1层编码(HTML 实体编码)
     &#x25;&#x35;&#x63;&#x25;&#x37;&#x35;...
     ↓
     最终 payload

💡 这个案例揭示了浏览器解析的本质 :三层解析器是串联 的,每层只处理自己"认识"的编码。攻击者可以给每层"喂"它认识的食物,搭建一条从 HTML → URL → JS 的编码通道。单层的 htmlspecialchars 只能防第一层(HTML),防不住第二层和第三层。


🅴 组:属性值中的实体解码 + JS 执行(案例 7)

html 复制代码
<button onclick="confirm('7&#39;)">Button</button>

结果:⚠️ 取决于后续处理,存在风险

解析过程:

  1. HTML 解析器看到 onclick 属性值为 confirm('7&#39;)
  2. 在 HTML 属性值中,字符引用会被解码
  3. &#39;'(单引号)
  4. 属性值解码后变为:confirm('7')
  5. 传给 JS 解析器后:confirm('7')------字符串 '7'' 闭合,语法成立

这个案例的核心意义在于:HTML 实体在属性值中会被解码,解码后的结果可能改变 JS 的语法结构。

攻击思路拓展:

复制代码
<button onclick="confirm('7&#39;)+alert(1)//')">Button</button>

htmlspecialchars 不会转义单引号 '(默认只转义双引号),但如果用了 ENT_QUOTES,它会转义 '&#039;。看起来安全了?

攻击者可以反过来用 HTML 实体解码 这个 &#039;,让它变回 ',然后闭合 JS 字符串。

💡 启示 :属性值中的 HTML 实体解码发生在 JS 解析之前。这意味着攻击者可以在属性值中用 HTML 实体"隐藏"JS 语法符号(引号、括号等),等 HTML 解码后再进入 JS 上下文执行。对属性值做 htmlspecialchars 只能防止 HTML 标签注入,不能保证 JS 上下文的安全。


四、一张表总结全部 15 个案例

编号 涉及解析层 Payload 位置 编码类型 是否成功 一句话原因
URL href 属性值 URL 编码协议字母 URL 解析器先查协议名,不解码
HTML→URL href 属性值 HTML 实体 + URL 编码 HTML 解码露出 javascript:,URL 解码执行
URL href 属性值 URL 编码冒号 URL 解析器找不到 : 字面字符
HTML div 文本 HTML 实体 数据状态下实体解码不创建标签
HTML textarea HTML 实体 RCDATA 状态只解码不解析标签
HTML textarea 无编码 RCDATA 状态下只认 </textarea>
HTML→JS onclick 属性值 HTML 实体 ⚠️ 有风险 属性值中实体先解码,改变 JS 语法
JS onclick 属性值 Unicode ' 是字符串内字符,不闭合引号
HTML script 标签内 HTML 实体 原始文本元素不解码实体
JS script 标签内 Unicode 字母 JS 允许标识符使用 Unicode 转义
JS script 标签内 Unicode 全编码 符号不能使用 Unicode 转义
JS script 标签内 Unicode 数字 数字字面量不支持 Unicode 转义
JS script 标签内 Unicode 控制字符 字符串内任意字符可转义
HTML→URL→JS href 属性值 三重嵌套 三层解码各管各的,完美串联

五、防御建议

理解了这三层解析机制,就能针对性地进行防御。以下是逐层的防护方案:

1. 为每个上下文选择合适的编码

上下文类型 编码方式 PHP 函数 说明
HTML 文本(<div>...</div> HTML 实体编码 htmlspecialchars($input, ENT_QUOTES) 转义 < > & " '
HTML 属性(<div class="..."> HTML 实体编码 htmlspecialchars($input, ENT_QUOTES) 同上,还要注意属性值引号
URL(<a href="..."> URL 编码 urlencode($input) 但最好不要把用户输入放到协议位置
JS 字符串 JS 字符串转义 CUSTOM \\\, '\', \n\\n
CSS CSS 转义 不建议直接拼接 尽量避免用户输入进入 CSS

2. 对关键属性做白名单校验

php 复制代码
// href / src / action 等属性:只允许 http / https
$allowed_schemes = ['http', 'https'];
$url = parse_url($input, PHP_URL_SCHEME);
if (!in_array($url, $allowed_schemes)) {
    die('不支持的协议类型');
}
// 或者更简单:
if (preg_match('/^(https?:\/\/)/i', $input)) {
    // 安全
}

这样可以彻底终结 javascript:data:vbscript: 等协议的绕过。

3. 设置 Content-Security-Policy (CSP)

这是最底层的防御手段,即使代码层面有漏洞,CSP 也可以作为最后一道防线:

nginx 复制代码
# 禁止内联脚本和事件处理器
Content-Security-Policy: default-src 'self'; script-src 'self';
复制代码
# 更严格:只允许从指定域名加载 JS
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com;

4. 避免在危险位置直接拼接用户输入

危险位置清单:

复制代码
❌ <a href="用户输入">           → URL 上下文,可执行 javascript:
❌ <img src="用户输入">           → URL 上下文
❌ <div onclick="用户输入">        → JS 上下文
❌ <script>用户输入</script>      → JS 上下文(原始文本)
❀ <div>用户输入</div>             → HTML 文本上下文(相对安全,仍有风险)

安全做法:

php 复制代码
// 安全:用户输入作为文本内容
echo '<div>' . htmlspecialchars($input) . '</div>';

// 安全:用户输入作为 URL 参数值(对 URL 做整体校验)
$safe_url = filter_var($input, FILTER_VALIDATE_URL);
if ($safe_url && preg_match('/^https?:\/\//', $safe_url)) {
    echo '<a href="' . htmlspecialchars($safe_url) . '">链接</a>';
}

// 不安全:用户输入作为 JS 事件监听值
// ❌ echo '<div onclick="' . $input . '">...</div>';
// ✅ 改用 addEventListener 从 JS 中动态绑定

5. 使用专业的上下文编码库

不要自己手写编码函数。OWASP 提供了成熟的库:

  • OWASP Java Encoder(Java)
  • OWASP ESAPI(多语言)
  • Twig / Blade / Smarty 等模板引擎内置了上下文感知的自动转义

六、总结

回到最初的问题:为什么 htmlspecialchars 防不住这个 payload?

html 复制代码
<a href="&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3a;&#x61;&#x6c;&#x65;&#x72;&#x74;(1)">

因为浏览器解析 HTML 经历了三层解析器:HTML → URL → JavaScripthtmlspecialchars 只作用于第一层(转义 <>),而攻击者根本没有使用能被 htmlspecialchars 处理的字符------他用的都是 &#x; 等合法字符,在 HTML 层面被解码后露出了 javascript: 协议头,然后在 URL 解析层和 JS 解析层一路畅通。

记住三个核心结论:

  1. 解析是分层的,每层只处理自己的编码 ------HTML 处理 &#x;,URL 处理 %xx,JS 处理 \uXXXX
  2. 单层防御是不够的------必须在每个上下文(HTML 文本、属性值、URL、JS、CSS)使用对应的编码方案
  3. 白名单胜于黑名单 ------对关键属性做协议白名单校验(只允许 http/https),比尝试穷举所有编码绕过方式更可靠

这 15 个案例是理解浏览器解析机制的起点,不是终点。实际攻防中,攻击者可以组合更多编码方式。关键是掌握一个思维模型------"当前输入会经过哪些解析器?每个解析器会做什么处理?"------有了这个模型,遇到任何新绕过手法,你都能第一时间分析出它的路径。


相关推荐
不一样的少年_1 小时前
不用框架,手搓 AI Agent:(一) 先让它跑起来
前端·后端·agent
小牛itbull1 小时前
【译】为什么我使用 React 重构了 WordPress?
前端·react.js·重构
用户059540174461 小时前
AI Agent 记忆存储踩坑实录:这个问题让我排查了 3 天,最终用 pytest + Docker 实现秒级回归
前端·css
今日无bug1 小时前
Emmet 语法速成指南
前端·css·html
Hello.Reader1 小时前
Tailwind CSS v4.3 从入门到实战Vite 安装、响应式布局、暗黑模式与主题配置
前端·css
kisshyshy2 小时前
前端存储、表单异步提交与 this 指向完全指南
前端·javascript·html
cidy_982 小时前
Skills 技能库- JulyCode
前端
Csvn2 小时前
TypeScript 泛型约束的 3 层演进:从 `extends` 到条件类型到重载推导
前端
小四的小六2 小时前
AI产品翻车实录:模型总结幻觉的三大类型与检测方案
前端·openai·ai编程