浏览器解析机制与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. l → l,e → e,r → r,t → t
  5. alert → alert
  6. (10) 是正常的函数调用
  7. 执行 alert(10) ✅

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


案例⑪ ------ 符号也被 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 解码为 1,2 解码为 2
  4. 但问题是:JS 解析器在解析标识符时,1 → 1 作为标识符是合法的(数字可以出现在标识符中)
  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 → JavaScript 。htmlspecialchars 只作用于第一层(转义 < 和 >),而攻击者根本没有使用能被 htmlspecialchars 处理的字符------他用的都是 &、#、x、; 等合法字符,在 HTML 层面被解码后露出了 javascript: 协议头,然后在 URL 解析层和 JS 解析层一路畅通。

记住三个核心结论:

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

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


相关推荐
Helix250几秒前
Electron、WebView 框架与 HTA 对比:桌面应用选型、安全性与现代替代方案
前端·javascript·electron·webview·vbscript·hta·本地html应用
开开心心就好24 分钟前
办公软件卸载不干净?专用工具一键清残留
java·前端·人工智能·智能手机·kafka·excel·memcache
静默回滚40 分钟前
图片能显示,画进canvas却空白:像素面积上限
前端
补码缩写40 分钟前
canvas大图内存怎么算:宽×高×4只是第一份
前端
coding漫漫长路44 分钟前
img.decode()报错的图能画,load成功的却是空的
前端
不可能片场1 小时前
AI视频首尾帧控制 用参考图定住镜头
前端·electron
修炼的dance1 小时前
canvas最大能开多大:先看面积,再看导出格式
前端
薛一半1 小时前
React-Redux核心原理与容器组件实战
前端·javascript·react.js
做运维的阿瑞1 小时前
自连接、交叉连接与复杂 JOIN:一条问题链讲透
前端·数据库·mysql
言乐61 小时前
JavaScript概括前端原理
开发语言·前端·javascript·python·ecmascript