手把手系列:XSS 原理、分类与防御 day04(上)

手把手系列:XSS 原理、分类与防御 day04(上)

按 day03 结尾的预告,下一篇本来该讲盲注自动化。排骨把顺序调了:先把 XSS 这条线拉通,盲注的脚本化和 sqlmap 往后放。

SQL 注入打的是数据库,XSS 打的是浏览器。两套东西思路差得远,但都属于“注入”这个大类。趁 SQL 注入的手感还在,紧接着看它的另一个分支,比隔一篇再回来顺。

这一篇同前先讲原理,再打靶场。三种类型、上下文、危害、防御这几块捋清楚了,下篇再上 DVWA 挨个打一遍。

特别声明:本文内容仅供网络安全学习研究使用。严禁在未获得书面授权的情况下对任何非自有系统实施扫描、渗透、测试等行为,违规操作带来的一切法律责任由行为人自行承担。

一、关于xss

XSS 原名 Cross Site Scripting(跨站脚本),缩写本应为 CSS,但与层叠样式表 Cascading Style Sheets 的缩写撞车,所以改成 XSS。除缩写冲突,两者技术上毫无关系。

一句话:页面把用户输入当成代码执行了。

正常的情况:用户输入是数据,显示出来就完了。
出问题的情况:用户输入变成了 HTML 或 JS 语法的一部分,浏览器照着执行。

和 SQL 注入是同一个病根——数据和代码的边界被打破。区别只在“谁在执行”:SQL 注入由数据库执行,XSS 由浏览器执行。

浏览器没法帮你区分。它收到的是一个 HTML 字符串,里面哪段是模板、哪段是用户数据,它不知道,它只认语法。

所以问题从来不出在浏览器,出在服务端拼接字符串的那一下。

1.1 浏览器是怎么走到那一步的

把浏览器处理页面的过程拆开看:

收到 HTML 字符串
      ↓
词法分析,一个个识别标签
      ↓
边识别边构建 DOM 树
      ↓
遇到 <script>
      ↓
暂停解析,把整段交给 JS 引擎
      ↓
执行完,继续往下解析

关键在于中间那步:遇到 <script> 就执行,这是 HTML 规范规定的行为,不是缺陷。

那只要能让 HTML 里多出一段可执行的东西,它就会被执行。而“多出来”的方式只有一种——服务端把用户输入拼进 HTML 的时候,没有把输入当成数据。

举个最小的例子。假设页面代码是:

echo "<p>你好," . $name . "</p>";

用户输入 排骨,页面是:

<p>你好,排骨</p>

一切正常。

用户输入 <script>alert(1)</script>,页面就变成:

<p>你好,<script>alert(1)</script></p>

浏览器看到 <script>,暂停解析,执行 alert(1),然后继续。

注意这段输入在数据库里、在 HTTP 请求里,都只是普通字符。 它变成代码,是在“被拼进 HTML”那一刻。

二、按“注入的东西存在哪”分成三类

XSS 的分类,本质是问一个问题:恶意代码是从哪儿进到页面里的?

2.1 反射型(Reflected)

用户输入跟着请求来,服务端原样把它回显进响应,只在这一次响应里生效。

典型场景:搜索框回显你搜的词、错误页回显参数、登录失败的提示带上用户名。

攻击要成立,得让受害者点一个构造好的链接。所以反射型又叫“非持久型”,必须诱导点击。

攻击者把恶意链接发给受害者
      ↓
受害者点开
      ↓
请求带到服务器,服务器把 payload 回显进页面
      ↓
浏览器执行

2.2 存储型(Stored)

用户输入被存进数据库或文件,之后每个打开页面的用户都会触发。

典型场景:留言板、评论区、昵称、个人签名、后台的日志查看页。

这类不需要诱导点击——你以为自己只是打开一个正常页面。危害也是三类里最大的一档。

2.3 DOM 型(DOM-based)

前两类都有个共同点:payload 经过了服务端,出现在响应 HTML 里。

DOM 型不一样:服务端完全不参与。是浏览器端的 JS 主动从某个地方读取数据,再把它写进 DOM。

// 数据从 URL 里取
var keyword = location.hash.slice(1);

// 写进 DOM
document.getElementById("result").innerHTML = "你搜索的是:" + keyword;

这里有个细节值得注意:URL 里 # 后面的部分(fragment)不会发给服务器。

所以 DOM 型的 payload 可能根本不出现在任何 HTTP 请求和响应里。服务端日志看不到,WAF 也可能看不到——它只能拦它看得见的东西。

2.4 三类放在一起对比

维度 反射型 存储型 DOM 型
payload 存在哪 请求参数里 数据库 / 文件里 浏览器执行 JS 时才拼进 DOM
是否落库 否 是 通常不落
响应 HTML 里有 payload 吗 有 有 可能完全没有
需要诱导点击吗 需要 不需要 通常需要
影响范围 单个受害者 所有访问者 单个受害者
典型触发点 搜索回显、错误页 留言板、昵称、评论 前端路由、location.hash 渲染

2.5 DOM 型为什么容易漏掉

DOM 型最容易漏,原因有两个。

第一,服务端代码怎么审都看不出问题。 因为服务端确实没问题——它只是返回一段前端 JS,那段 JS 本身是固定的。

第二,漏洞出在“危险函数”上,而这些函数在正常业务里到处都是。

常见的一批:

位置 危险程度 说明
innerHTML / outerHTML 高 把字符串当 HTML 解析,标签会生效
document.write() 高 直接往文档流里写 HTML
eval() / new Function() 极高 把字符串当代码执行
setTimeout("字符串") / setInterval("字符串") 高 传字符串时按代码执行
location / location.href 中 可以走 javascript: 伪协议
jQuery 的 .html() / .append() 高 同 innerHTML
React 的 dangerouslySetInnerHTML 高 名字已经把风险写在脸上了
Vue 的 v-html 高 同上

相对安全的替代品是 textContent、innerText、createElement()——它们把内容当文本处理,不当 HTML。

记住这条区分就够了:凡是名字里带 html 的,基本都在解析 HTML;凡是叫 text 的,基本只当文本。 这一条能挡掉大量低级误用。

三、payload 长什么样,取决于它落在哪

这是 XSS 里最容易被忽略的一层。同一段输入,落在页面的不同位置,需要的写法完全不同。

因为你要做的第一件事是闭合——先让前面的语法结束,才能插进自己的东西。

数据落在哪 页面长这样 要闭合什么 一种写法
HTML 文本节点 <div>DATA</div> 不用闭合,直接插标签 <img src=x onerror=alert(1)>
双引号属性 <input value="DATA"> 先闭合双引号 " onmouseover="alert(1)
单引号属性 <input value='DATA'> 先闭合单引号 ' onmouseover='alert(1)
无引号属性 <input value=DATA> 用空格断开属性 x onmouseover=alert(1)
JS 字符串里 var a = "DATA"; 闭合引号并转义 ";alert(1);//
<script> 块里 <script>var a = DATA;</script> 不用闭合标签,直接写表达式 alert(1)
链接地址 <a href="DATA"> 换成伪协议 javascript:alert(1)
CSS 里 <div style="DATA"> 闭合后写声明 现代浏览器基本不执行 JS;历史上的 expression() 只在旧版 IE 有效

这张表解释了一件事:为什么“过滤 <script>”这种防护从根上就不够。

你可以不用 <script>。浏览器认的可执行入口远不止这一个:

  • <img src=x onerror=...> —— 图片加载失败时触发
  • <svg onload=...> —— SVG 加载时触发
  • <body onload=...>、<iframe srcdoc=...>
  • 各种 on 开头的事件属性:onmouseover、onfocus、onclick
  • <a href="javascript:..."> —— 伪协议

标签有几十个,事件有几百个。黑名单永远列不全。

四、危害:不只是弹个框

新手对 XSS 的印象常常停在 alert(1)。那个弹窗只是证明“代码能执行”,真正的危害在后面。

4.1 盗 Cookie,接管会话

如果 Cookie 没有 HttpOnly 标记,JS 能直接读:

document.cookie

读到之后往外发一份,攻击者拿着这个 Cookie 就成了你——不需要密码。

这也是为什么 HttpOnly 是第一道最便宜的防线。

4.2 以受害者身份操作

页面上能点的按钮、能提交的表单,攻击者的 JS 都能替他点、替他提。

发帖、改资料、转账、加好友、删数据——只要受害者有权限,JS 就有权限。

4.3 绕过 CSRF 防护

CSRF(Cross-Site Request Forgery,跨站请求伪造):你已登录某站,攻击者诱导你打开恶意页面,该页面偷偷向目标站发请求;浏览器自动带上你的 Cookie,服务器以为是你本人操作

CSRF这一条经常被单独忽略。

CSRF 的经典防御是“请求里带一个攻击者猜不到的 token”。但 token 是渲染在页面里的,而 XSS 的 JS 和页面同源——它可以直接把 token 读出来,再带着 token 发请求。

所以一个站点如果有 XSS,它的 CSRF 防护基本等于没有。

4.4 钓鱼与界面伪造

用 JS 在页面上盖一层假登录框,把密码骗到手。地址栏还是真域名,用户很难分辨。

4.5 键盘记录、剪贴板、摄像头

JS 能监听输入、读剪贴板(部分场景需要授权)、申请摄像头权限。一旦拿到同源执行权,能做的事远超一般人的想象。

4.6 蠕虫式传播

存储型 XSS 有个可怕的放大效应:被感染的用户访问页面时,会自动替攻击者发帖。

2005 年 MySpace 的 Samy 蠕虫是经典案例——利用存储型 XSS 自我复制,不到 20 小时感染了超过一百万用户。

一个 XSS 漏洞,加上“自动提交”这个动作,就能变成蠕虫。

五、几个常见的误判

想法 为什么不对
过滤 <script> 就行了 大小写、嵌套、其他标签、事件属性,绕过方式太多
我只处理 GET,POST 是安全的 POST 一样会被回显,改包就能伪造
前端做了校验就够了 前端校验只拦浏览器,直接发请求完全绕过
转义一次就能到处用 不同上下文要不同编码,HTML 转义放进 <script> 里没用
有 HttpOnly 就不怕了 它只防读 Cookie,页面内容、token、操作照样能被操纵
上了 CSP 就高枕无忧 CSP 是缓解层,配错了(比如允许 unsafe-inline)等于没有
这是老漏洞,现在少了 OWASP Top 10 2021 里它并进了 A03 注入类,依然在高位

最后一条补充一句:现代框架确实把 XSS 的发生率压下去了(React、Vue 默认把插值当文本处理),但没有消灭它。因为框架都留了“逃生门”——dangerouslySetInnerHTML、v-html、innerHTML。用上这些,防护就回到了手工时代。

六、防御:在输出处编码,不在输入处过滤

这是整篇最该带走的一句话。

为什么不在输入处过滤? 因为同一份数据,可能出现在多个输出位置。你在录入时把它转义成 HTML 实体,等到它要进 JS 字符串时又不对了。

正确的位置是每一个输出点,按它落地的上下文选编码方式:

输出到哪 用什么编码
HTML 文本 HTML 实体编码(&lt; &gt; &amp; &quot;)
HTML 属性 同上,并且给属性加引号
JS 字符串 JS 转义(\u003c 等),或者干脆用 JSON.stringify 把值序列化
URL 参数 URL 编码(encodeURIComponent)
CSS 尽量避免动态拼接;必须拼时做 CSS 转义

配套的其他几层:

措施 作用 说明
输出编码 根治 按上下文编码,让数据永远只是数据
输入校验(白名单) 收窄 限定类型、长度、字符集;黑名单永远不全
CSP 纵深 script-src 'self' 'nonce-xxx',禁掉内联脚本
Cookie 加 HttpOnly 减损 就算被 XSS,也读不到会话 Cookie
SameSite 减损 降低跨站请求被带上的概率
成熟 sanitizer 富文本场景 需要允许 HTML 时用 DOMPurify 这类库,别自己写正则

富文本那一栏值得多说一句。业务上经常“必须允许用户发带格式的内容”,这时候不能简单转义掉一切。正确做法是白名单式净化——只保留允许的标签和属性,其余全部剥掉。这件事看起来简单,实际上绕过手法极多,用成熟库,不要自己写正则。

6.1 一个对照

// 错误:直接拼入
echo "<div>" . $_GET['name'] . "</div>";
// 正确:输出时按上下文编码
echo "<div>" . htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8') . "</div>";

htmlspecialchars 会把 <、>、&、"、' 转成实体,浏览器看到的是文本,不是标签。

注意 ENT_QUOTES 这个参数——它把单引号和双引号一起处理。如果你的数据可能落进属性里,这个参数不能省。

七、和 CSRF 的关系

这两个经常被混在一起问,其实攻击模型不同:

XSS CSRF
本质 在受害者浏览器里执行攻击者的代码 借受害者的登录状态发一个请求
需要读响应吗 需要,也能读 不需要
需不需要诱导点击 反射型/DOM 型需要,存储型不需要 需要
防御核心 输出编码 + CSP CSRF token + SameSite + 校验 Origin

结论是:XSS 的危害覆盖 CSRF。 有 XSS 的站点,CSRF token 是能读的;反过来,有 CSRF 防护的站点不代表没有 XSS。

八、下篇预告

原理到这。下篇上 DVWA,三种类型各打一遍:

  • 反射型:从低到高四档,看过滤怎么一层层加,又怎么一层层被绕过
  • 存储型:验证“不需要诱导点击”这件事到底有多致命
  • DOM 型:为什么它可能在服务端日志里完全看不到
  • 最后统一看正确的防御写法

下篇见。

参考

posted @ 2026-10-23 19:49  半扇排骨  阅读(9)  评论(0)    收藏  举报