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)之后,三个全部变成实体:<script>、<img ...>、
" onmouseover="。
第三个最能说明问题:它里面一个尖括号都没有。
它靠的是提前闭合属性——所以「只转义尖括号」是不够的,引号也必须转。
这就是「按上下文转义」的意思:写进 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>
转义:<script>alert(1)</script>
原始:<img src=x onerror=alert(1)>
转义:<img src=x onerror=alert(1)>
原始:" onmouseover="alert(1)
转义:" onmouseover="alert(1)
用了什么 html.escape(& < > " ' 五个字符)
三种 payload 不转义时 全部原样进入 HTML
三种 payload 转义后 全部变成实体,不再有 <
注意第三种:`" onmouseover="alert(1)` 里一个尖括号都没有。
它靠的是**提前闭合属性**,所以只转义尖括号是不够的——引号也必须转。这就是「按上下文转义」的意思:写进 HTML 文本和写进属性,要处理的字符不一样。
不转义那一列三行全是「进去了」;转义那一列三行全是「没进去」。差别只是一个函数调用。
逐条对照更清楚:原始是 <script>alert(1)</script>,
转义后是 <script>alert(1)</script>——字节变多了,语义变安全了。
第三条 " onmouseover="alert(1) 转义后是 " onmouseover="alert(1)。
它本来一个尖括号都没有,靠引号就能逃出去——所以只转 < 和 > 是漏的。
html.escape 处理的是五个字符:&、<、>、"、'。
这五个字符在 HTML 文本和属性值里都够用,但放进 JS 变量里还不够——那得用另一套规则。
30 秒实验
- 把 payload 塞进一个
<a href="...">的属性里,看要额外转义什么(javascript:协议)。 - 在浏览器里打开一个带
Content-Security-Policy: default-src 'self'的页面,看控制台报什么。 - 想一想:为什么「过滤掉
script这个词」不管用?举一个不用script的 payload。
常见误区
- 「转义尖括号就够了」——属性里的引号也能逃出去,第三个 payload 就是例子。
- 「XSS 只影响前端,服务器没事」——对,但受害的是用户,而且要赔的是你。
- 「前端框架会自动转义,所以安全」——它给你留了插入原始 HTML 的口子。
自测三问(点开对答案)
1. XSS 的成因一句话是什么? —— 用户输入被当成了 HTML 的一部分,于是数据变成了代码。 2. 反射型、存储型、DOM 型有什么区别? —— payload 分别来自 URL 回显、数据库、前端脚本自己插入。 3. 为什么只转义尖括号不够? —— 属性值里的引号也能提前闭合属性,注入事件处理器。XSS 打的是页面。下一个更狠:输入直接进了 SQL 语句。
- ☐ (本节完,下一节 11.2.2 SQL注入攻击)