🛡️ 第四章 系统漏洞的防范
📄 一句话概括

跨网站式脚本攻击(XSS,跨站脚本)的原理是:网站把一个用户的输入原样显示给了另一个用户,浏览器分不清这段内容"是网站写的"还是"是别人写的",于是把它当成网站的代码执行了——它不是攻击网站,而是攻击网站的访问者;它借用的不是服务器的权限,而是受害者浏览器里的信任。

📝 本节和前面几节的关系

4-4 讲的是"输入被当成 SQL 代码",这一节讲的是同一件事的另一个落点:输入被当成网页代码。

两节的原理可以合并成一句话:凡是"把用户输入直接送进某个有解释能力的引擎(数据库或浏览器)"的地方,都会出问题。 区别在于防法不同:SQL 那条路靠"参数化"(数据不进入代码),这条路的正确解法是"输出转义"(数据进入代码时要先改变它的形式)——注意是"输出时转义",不是"输入时过滤",这个顺序很多人都搞反了。

一、XSS 到底"跨"了什么

教材把这一节叫"跨越多个网站的攻击",名字里的"跨"指的是恶意脚本的来处和它的执行现场不在同一个地方:

所以 XSS 的攻击对象不是服务器,而是"正在访问服务器的人"。它危害大的原因是:这段脚本是在"网站自己"的名下运行的——浏览器和用户都很难把它和网站的正常代码区分开。

二、三种 XSS,落点完全不同

教材没有细分,但实际防御必须分,因为三种的修法不一样:

类型 恶意脚本存在哪 怎么被触发
存储型 服务器数据库里 任何人打开那个页面都中招
反射型 URL 参数里 点开那条特制链接才中招
DOM 型 浏览器端由 JS 拼出 服务器看不见,扫描器也难看

三种里最危险的是存储型:攻击者只提交一次,之后每个访问这个页面的用户都会中招——它不需要受害者做任何事,只要"来访问"就够了。

最难查的是 DOM 型:恶意内容从来没有经过服务器,是浏览器端的 JavaScript 自己把用户输入拼进页面的——所以服务器日志里干干净净,漏洞扫描器看到的是"服务器返回的正常页面",也抓不到。

三、动手 Demo:一段输入,三种落点

下面这段脚本演示了"转义"这个动作到底做了什么,以及三种 XSS 的落点差别:

# 用户输入的内容(正常写法下它就是一串普通文字)
raw = "<script>alert(1)</script>"

# 未转义:直接放进 HTML,浏览器把它当代码执行
print("未转义输出:", raw)

# 转义:把 < > 换成实体,浏览器把它当文字显示
esc = raw.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
print("转义后输出:", esc)

实跑输出:

一、先看转义到底做了什么
  环节              内容
  ----------------------------------------------------------------------------------------------
  用户输入          <script>alert(1)</script>
  未转义输出        <script>alert(1)</script>    ← 浏览器当代码执行
  转义后输出        &lt;script&gt;alert(1)&lt;/script&gt;    ← 浏览器当文字显示

二、三种 XSS,落点完全不同
  类型            恶意脚本存在哪              怎么被触发
  ----------------------------------------------------------------------------------------------
  存储型          服务器数据库里              任何人打开那个页面都中招
  反射型          URL 参数里                  点开那条特制链接才中招
  DOM 型          浏览器端由 JS 拼出          服务器看不见,扫描器也难看

  → 三种的防御并不相同:存储型靠【输出转义】,
     DOM 型靠【别把用户输入当代码来拼】。

三、CWE-79 的位置(2026)
  累计排名      第 1(全部 CVE 里出现次数最多的一类)
  2026 上半年    669 个(奇安信口径,第三名;2025 年是 1,396 个)
  插件生态里     WordPress 插件单周 XSS 94 条,仍是第一名

读这个输出:

四、⚠️ 2026 现状

30 秒实验

今天做两个动作,各 15 秒:

  1. 打开任何一个有"评论"或"昵称"功能的页面,输入一段带尖括号的普通文字,比如 <b>test</b> 或 1<2>3,然后看它显示成什么样。如果页面上显示的是"加粗的 test"或者"少了几个字符",那说明尖括号被当成 HTML 解析了(存在 XSS 的可能);如果显示的是原样的 <b>test</b> 这串字符,说明转义做对了。这个测试比敲 alert(1) 温和得多,但判断的是同一件事。
  2. 在浏览器里随便打开一个网页,按 F12 打开开发者工具,看 Application(或存储)那一栏里的 Cookie。找一找有没有 HttpOnly 这一列打了勾。如果关键的会话 Cookie 没有 HttpOnly,那么一旦发生 XSS,攻击者可以直接用脚本把会话令牌抄走。 这一步不是让你去改什么,而是让你亲眼看到"XSS 的收益有多大"取决于这些属性有没有设——这也正好是下一节(4-7)要讲的东西。

这一节要带走的一句是:XSS 不是网站被打了,而是网站"替攻击者把话说了出来"——所以它的修复位置在"输出",而不在"输入"。

常见误区

  1. 错误:在输入的时候把 <script> 过滤掉,就防住 XSS 了。——纠正:XSS 的正确防线在"输出"这一步,而不是"输入"这一步。原因很实在:同一份用户输入,可能要在好几个地方显示——网页上要转义 HTML、放进 JavaScript 里要转义成 JS 字面量、放进 CSS 或 URL 里又是另一套规则。如果在输入时统一过滤,你根本不知道它之后要用在哪里,只能选择过滤得最狠(→ 正常内容被破坏)或最松(→ 漏洞仍在)。 正确做法是"输入时校验格式(比如长度、类型),输出时按上下文转义"——这也解释了为什么现代框架默认都会在模板里自动转义。
  2. 错误:我的网站没有"用户发帖"功能,不会有 XSS。——纠正:存储型 XSS 需要"能把内容存进去",但反射型和 DOM 型不需要。任何"把 URL 参数显示回页面上"的地方都算——比如带有搜索关键词回显的页面("没有找到关于 XXX 的结果")、带错误信息回显的页面、带跳转提示的页面。 而 DOM 型的入口往往更隐蔽:前端框架里用 innerHTML 而不是 textContent 拼接、用 document.write、把 URL 的 hash 直接当内容渲染——这些地方服务器完全看不到,但都是教科书级的 DOM 型 XSS。
  3. 错误:XSS 最多弹个框,危害不大。——纠正:弹框(alert(1))只是漏洞验证用的最温和演示。真实的 XSS 能做的是: ① 读取当前页面上的敏感内容(表单里还没提交的信息、页面显示的订单号);② 以受害者的身份发起操作(改密码、转账、发消息);③ 把页面整个替换成一个假的登录框(骗密码);④ 把用户从电商页引向诈骗页。它之所以危险,是因为脚本是在"网站自己"的名下运行的——用户看到的地址栏是真实的域名,锁头标志也在,任何"看地址栏"的直觉都失效了。
  4. 错误:现代浏览器和框架已经解决了 XSS,不用管了。——纠正:现代默认值收窄了 XSS 的收益,没有消除 XSS 本身。证据就在统计里:2026 年上半年 XSS 仍有 669 个新报漏洞,在插件生态里还是第一名。而且框架的默认保护是可以被"绕过"的:显式使用"不转义"的输出方式(各种 raw/innerHTML 类接口)、把用户输入拼进前端模板、用前端框架的"动态 HTML 插入"功能——这些都是把默认保护关掉的操作,而它们非常常见。 所以准确的判断是:框架降低了"不小心犯错"的概率,但没有取消"知道自己在做什么"的责任。
自测三问(点开对答案) 1. 为什么说 XSS "不是攻击网站,而是攻击网站的访问者"? —— **因为恶意脚本执行的位置是受害者的浏览器**,**它借用的身份是"受害者在那个网站上的会话"**。**服务器从头到尾没有异常行为:它只是忠实地把一段内容返回给了页面**(这也是 XSS 难以在服务器侧发现的原因)。**这就带来一个重要的推论:XSS 的"后果范围"由受害者的身份决定**——**如果被他钓到的是普通用户,影响限于个人;如果点开的是管理员,攻击者就相当于拿到了后台**。**所以"存储型 XSS + 管理员会浏览那个页面"是一种非常常见的高危组合。** 2. 为什么 XSS 的正确修复位置是"输出"而不是"输入"? —— **因为同一份数据要被用在多个不同的上下文里,而每个上下文的转义规则都不一样**。**放进 HTML 正文要转 `& < >`;放进 HTML 属性里还要处理引号;放进 `