📘 第十一章 Web的攻击技术
📄 一句话概括

XSS 的成因只有一句:用户输入被当成了 HTML 的一部分。 三种 payload 在不转义时全部原样进入页面,转义之后全部变成实体。 难点在于「按上下文转义」——引号也必须处理。

📝 本节和前面几节的关系

上一节把攻击分成两类,这一节看被动攻击里最出名的那个。它的修补往往只差一行函数调用。

一、三种形态

反射型:payload 在 URL 里,服务端把它原样回显到页面上。 攻击者要把构造好的链接发给受害者,受害者一点就中。 存储型:payload 被存进数据库(评论、昵称、留言),以后每个打开那个页面的人都会中。 危害大得多,因为它不需要骗人点链接。 DOM 型:服务端根本没参与——前端脚本把 URL 里的片段直接塞进 innerHTML,浏览器一解析就执行了。 这一种光在服务端转义是挡不住的。 三种的共同点是一样的:数据被当成了代码。区别只在「谁把它放进去的」。

二、转义这一行

Demo 里三个 payload:<script>alert(1)</script>、<img src=x onerror=alert(1)>、 " onmouseover="alert(1)。 不转义时,三个全部原样进入 HTML。 转义(html.escape)之后,三个全部变成实体:&lt;script&gt;、&lt;img ...&gt;、 &quot; onmouseover=&quot;。 第三个最能说明问题:它里面一个尖括号都没有。 它靠的是提前闭合属性——所以「只转义尖括号」是不够的,引号也必须转。 这就是「按上下文转义」的意思:写进 HTML 文本、写进属性值、写进 <script> 里的 JS 变量、写进 CSS、 写进 URL——五个位置,要转的字符各不相同,规则也不相通。

三、除了转义还能做什么

CSP(内容安全策略)是一道兜底:告诉浏览器「本页只准执行这些来源的脚本」。 就算有 payload 混进来,它加载不了外部脚本、也执行不了内联脚本。 HttpOnly 让 cookie 读不到:XSS 最常见的目的是偷会话 ID,标了 HttpOnly 的 cookie 脚本拿不到, 这一条路就断了。 框架的自动转义:现在的主流前端框架默认就会转义插值, 危险的是那个「我要插入原始 HTML」的专用方法。搜代码里那些方法,比逐个审查拼接更有效。 三条一起上才稳。转义是主力,CSP 和 HttpOnly 是「万一漏了一个」的保险。

四、动手 Demo:三种 payload,转义前后

两个一模一样的搜索引擎,唯一区别是一个用 html.escape、一个不用。 各喂三个 payload,看它们有没有原样出现在返回的 HTML 里。

def page_of(word, escape):
    shown = html.escape(word) if escape else word
    return u'<html><body><p>你搜的是:%s</p></body></html>' % shown

CASES = [(u'文本里塞脚本', u'<script>alert(1)</script>'),
         (u'用事件属性', u'<img src=x onerror=alert(1)>'),
         (u'引号逃出属性', u'" onmouseover="alert(1)')]

实跑输出:

--------------------------------------------------------------------
XSS:转义和不转义,差一行函数
--------------------------------------------------------------------
  手法               payload                        不转义      转义
  文本里塞脚本           <script>alert(1)</script>      进去了      没进去
  用事件属性            <img src=x onerror=alert(1)>   进去了      没进去
  引号逃出属性           " onmouseover="alert(1)        进去了      没进去

  逐条看转义之后的样子:
    原始:<script>alert(1)</script>
    转义:&lt;script&gt;alert(1)&lt;/script&gt;
    原始:<img src=x onerror=alert(1)>
    转义:&lt;img src=x onerror=alert(1)&gt;
    原始:" onmouseover="alert(1)
    转义:&quot; onmouseover=&quot;alert(1)

  用了什么                   html.escape(& < > " ' 五个字符)
  三种 payload 不转义时      全部原样进入 HTML
  三种 payload 转义后        全部变成实体,不再有 <

  注意第三种:`" onmouseover="alert(1)` 里一个尖括号都没有。
  它靠的是**提前闭合属性**,所以只转义尖括号是不够的——引号也必须转。这就是「按上下文转义」的意思:写进 HTML 文本和写进属性,要处理的字符不一样。

不转义那一列三行全是「进去了」;转义那一列三行全是「没进去」。差别只是一个函数调用。 逐条对照更清楚:原始是 <script>alert(1)</script>, 转义后是 &lt;script&gt;alert(1)&lt;/script&gt;——字节变多了,语义变安全了。 第三条 " onmouseover="alert(1) 转义后是 &quot; onmouseover=&quot;alert(1)。 它本来一个尖括号都没有,靠引号就能逃出去——所以只转 < 和 > 是漏的。 html.escape 处理的是五个字符:&、<、>、"、'。 这五个字符在 HTML 文本和属性值里都够用,但放进 JS 变量里还不够——那得用另一套规则。

30 秒实验

  1. 把 payload 塞进一个 <a href="..."> 的属性里,看要额外转义什么(javascript: 协议)。
  2. 在浏览器里打开一个带 Content-Security-Policy: default-src 'self' 的页面,看控制台报什么。
  3. 想一想:为什么「过滤掉 script 这个词」不管用?举一个不用 script 的 payload。

常见误区

  1. 「转义尖括号就够了」——属性里的引号也能逃出去,第三个 payload 就是例子。
  2. 「XSS 只影响前端,服务器没事」——对,但受害的是用户,而且要赔的是你。
  3. 「前端框架会自动转义,所以安全」——它给你留了插入原始 HTML 的口子。
自测三问(点开对答案) 1. XSS 的成因一句话是什么? —— 用户输入被当成了 HTML 的一部分,于是数据变成了代码。 2. 反射型、存储型、DOM 型有什么区别? —— payload 分别来自 URL 回显、数据库、前端脚本自己插入。 3. 为什么只转义尖括号不够? —— 属性值里的引号也能提前闭合属性,注入事件处理器。

XSS 打的是页面。下一个更狠:输入直接进了 SQL 语句。

相关笔记