跨网站式脚本攻击(XSS,跨站脚本)的原理是:网站把一个用户的输入原样显示给了另一个用户,浏览器分不清这段内容"是网站写的"还是"是别人写的",于是把它当成网站的代码执行了——它不是攻击网站,而是攻击网站的访问者;它借用的不是服务器的权限,而是受害者浏览器里的信任。
4-4 讲的是"输入被当成 SQL 代码",这一节讲的是同一件事的另一个落点:输入被当成网页代码。
两节的原理可以合并成一句话:凡是"把用户输入直接送进某个有解释能力的引擎(数据库或浏览器)"的地方,都会出问题。 区别在于防法不同:SQL 那条路靠"参数化"(数据不进入代码),这条路的正确解法是"输出转义"(数据进入代码时要先改变它的形式)——注意是"输出时转义",不是"输入时过滤",这个顺序很多人都搞反了。
一、XSS 到底"跨"了什么
教材把这一节叫"跨越多个网站的攻击",名字里的"跨"指的是恶意脚本的来处和它的执行现场不在同一个地方:
- 脚本存在 A 网站(攻击者把恶意代码提交到 A 网站的一个页面里);
- 它在受害者的浏览器里执行,而此刻浏览器正在B 网站(或者就在 A 网站)的页面上下文中;
- 因此这段脚本能拿到的是"受害者在那个页面上的身份"——Cookie、会话、页面内容、能发起的请求。
所以 XSS 的攻击对象不是服务器,而是"正在访问服务器的人"。它危害大的原因是:这段脚本是在"网站自己"的名下运行的——浏览器和用户都很难把它和网站的正常代码区分开。
二、三种 XSS,落点完全不同
教材没有细分,但实际防御必须分,因为三种的修法不一样:
| 类型 | 恶意脚本存在哪 | 怎么被触发 |
|---|---|---|
| 存储型 | 服务器数据库里 | 任何人打开那个页面都中招 |
| 反射型 | URL 参数里 | 点开那条特制链接才中招 |
| DOM 型 | 浏览器端由 JS 拼出 | 服务器看不见,扫描器也难看 |
三种里最危险的是存储型:攻击者只提交一次,之后每个访问这个页面的用户都会中招——它不需要受害者做任何事,只要"来访问"就够了。
最难查的是 DOM 型:恶意内容从来没有经过服务器,是浏览器端的 JavaScript 自己把用户输入拼进页面的——所以服务器日志里干干净净,漏洞扫描器看到的是"服务器返回的正常页面",也抓不到。
三、动手 Demo:一段输入,三种落点
下面这段脚本演示了"转义"这个动作到底做了什么,以及三种 XSS 的落点差别:
# 用户输入的内容(正常写法下它就是一串普通文字)
raw = "<script>alert(1)</script>"
# 未转义:直接放进 HTML,浏览器把它当代码执行
print("未转义输出:", raw)
# 转义:把 < > 换成实体,浏览器把它当文字显示
esc = raw.replace("&", "&").replace("<", "<").replace(">", ">")
print("转义后输出:", esc)
实跑输出:
一、先看转义到底做了什么
环节 内容
----------------------------------------------------------------------------------------------
用户输入 <script>alert(1)</script>
未转义输出 <script>alert(1)</script> ← 浏览器当代码执行
转义后输出 <script>alert(1)</script> ← 浏览器当文字显示
二、三种 XSS,落点完全不同
类型 恶意脚本存在哪 怎么被触发
----------------------------------------------------------------------------------------------
存储型 服务器数据库里 任何人打开那个页面都中招
反射型 URL 参数里 点开那条特制链接才中招
DOM 型 浏览器端由 JS 拼出 服务器看不见,扫描器也难看
→ 三种的防御并不相同:存储型靠【输出转义】,
DOM 型靠【别把用户输入当代码来拼】。
三、CWE-79 的位置(2026)
累计排名 第 1(全部 CVE 里出现次数最多的一类)
2026 上半年 669 个(奇安信口径,第三名;2025 年是 1,396 个)
插件生态里 WordPress 插件单周 XSS 94 条,仍是第一名
读这个输出:
- 第一部分点出了整件事的开关:同样一段文字,"放进 HTML"和"作为文字显示"是两种完全不同的结果。转义做的事很朴素——把
<换成<,于是浏览器不再认为这是"标签的开始",只当成一个普通的字符。所以 XSS 的修复往往不是"加一道防护",而是"在输出的时候多转一次"。 - 第二部分是很多人会漏的一层:三种 XSS 的修法不同。存储型在"输出"这一步转义就能解决(因为源头是数据库);DOM 型必须改 JavaScript 的写法(不能用 innerHTML 拼用户输入)。只知道"XSS 要转义"、却把内层写法和外层混着处理,就会出现"服务器端做得很好、前端照样中招"的情况。
- 第三部分的排名值得注意:XSS(CWE-79)在"全部 CVE 里出现次数最多"这个口径上排名第一。这个"累计第一"和"2026 上半年 669 个"是两回事——前者说的是它历史总量最大(存在了二十多年、影响面最广),后者说的是近期的增量。两个数字都对,但含义不同,看漏洞统计时要注意口径。
四、⚠️ 2026 现状
- 【XSS 仍是"数量最大的一类",但新增在放缓】。 2026 年上半年统计中 XSS(CWE-79)669 个,排在 SQL 注入(1,116)和释放后重用(858)之后;2025 年同一口径是 1,396 个。但它在"全部 CVE 累计出现次数"这个口径上依然是第一名——原因是它覆盖面最广:任何"把用户输入显示在页面上"的地方都算。
- 【插件生态里,XSS 是第一大类】。 2026 年第一季度 WordPress 插件新增漏洞 2,738 个;一份单周 358 条的清单里,XSS 94 条排第一(4-12)。原因是很多插件提供"让用户自定义显示内容"的功能,而这类功能天生就是 XSS 的入口。
- 【OWASP 榜单里,XSS 的位置被"上移"了】。 2021 版里 XSS 是独立的一项(A03 注入类里的代表),而 2025 版把注入类整体降到第 5,同时把"访问控制失效"(吸收 SSRF 后仍排第一)和"安全配置错误"(从第 5 升到第 2)提到前面。这个变化不是说 XSS 不重要了,而是说"更基础的配置类问题"成了更常见的原因(4-10)。
- 【XSS 在现代浏览器里"能做的事"被收窄了,但没有消失】。 HttpOnly 属性让 JS 读不到 Cookie、各种内容安全策略(CSP)限制了脚本来源、SameSite 让跨站请求不再自动带 Cookie(4-7)——这些默认值确实让 XSS 的"收益"下降了不少。但它依然是有效的:可以用来伪造页面、诱导操作、读取页面上的敏感信息、把用户引向钓鱼页——所以在"多一层防线"的思路下,XSS 仍然必须从输出转义这一层堵住,不能指望浏览器的默认值兜底。
30 秒实验
今天做两个动作,各 15 秒:
- 打开任何一个有"评论"或"昵称"功能的页面,输入一段带尖括号的普通文字,比如
<b>test</b>或1<2>3,然后看它显示成什么样。如果页面上显示的是"加粗的 test"或者"少了几个字符",那说明尖括号被当成 HTML 解析了(存在 XSS 的可能);如果显示的是原样的<b>test</b>这串字符,说明转义做对了。这个测试比敲alert(1)温和得多,但判断的是同一件事。 - 在浏览器里随便打开一个网页,按 F12 打开开发者工具,看 Application(或存储)那一栏里的 Cookie。找一找有没有 HttpOnly 这一列打了勾。如果关键的会话 Cookie 没有 HttpOnly,那么一旦发生 XSS,攻击者可以直接用脚本把会话令牌抄走。 这一步不是让你去改什么,而是让你亲眼看到"XSS 的收益有多大"取决于这些属性有没有设——这也正好是下一节(4-7)要讲的东西。
这一节要带走的一句是:XSS 不是网站被打了,而是网站"替攻击者把话说了出来"——所以它的修复位置在"输出",而不在"输入"。
常见误区
- 错误:在输入的时候把
<script>过滤掉,就防住 XSS 了。——纠正:XSS 的正确防线在"输出"这一步,而不是"输入"这一步。原因很实在:同一份用户输入,可能要在好几个地方显示——网页上要转义 HTML、放进 JavaScript 里要转义成 JS 字面量、放进 CSS 或 URL 里又是另一套规则。如果在输入时统一过滤,你根本不知道它之后要用在哪里,只能选择过滤得最狠(→ 正常内容被破坏)或最松(→ 漏洞仍在)。 正确做法是"输入时校验格式(比如长度、类型),输出时按上下文转义"——这也解释了为什么现代框架默认都会在模板里自动转义。 - 错误:我的网站没有"用户发帖"功能,不会有 XSS。——纠正:存储型 XSS 需要"能把内容存进去",但反射型和 DOM 型不需要。任何"把 URL 参数显示回页面上"的地方都算——比如带有搜索关键词回显的页面("没有找到关于 XXX 的结果")、带错误信息回显的页面、带跳转提示的页面。 而 DOM 型的入口往往更隐蔽:前端框架里用
innerHTML而不是textContent拼接、用document.write、把 URL 的 hash 直接当内容渲染——这些地方服务器完全看不到,但都是教科书级的 DOM 型 XSS。 - 错误:XSS 最多弹个框,危害不大。——纠正:弹框(
alert(1))只是漏洞验证用的最温和演示。真实的 XSS 能做的是: ① 读取当前页面上的敏感内容(表单里还没提交的信息、页面显示的订单号);② 以受害者的身份发起操作(改密码、转账、发消息);③ 把页面整个替换成一个假的登录框(骗密码);④ 把用户从电商页引向诈骗页。它之所以危险,是因为脚本是在"网站自己"的名下运行的——用户看到的地址栏是真实的域名,锁头标志也在,任何"看地址栏"的直觉都失效了。 - 错误:现代浏览器和框架已经解决了 XSS,不用管了。——纠正:现代默认值收窄了 XSS 的收益,没有消除 XSS 本身。证据就在统计里:2026 年上半年 XSS 仍有 669 个新报漏洞,在插件生态里还是第一名。而且框架的默认保护是可以被"绕过"的:显式使用"不转义"的输出方式(各种
raw/innerHTML类接口)、把用户输入拼进前端模板、用前端框架的"动态 HTML 插入"功能——这些都是把默认保护关掉的操作,而它们非常常见。 所以准确的判断是:框架降低了"不小心犯错"的概率,但没有取消"知道自己在做什么"的责任。