Reflected Cross Site Scripting(XSS)反射型跨站脚本漏洞:用户输入回显缺陷与 DVWA 分级绕过实战
Reflected Cross Site Scripting(XSS)反射型跨站脚本漏洞:用户输入回显缺陷与 DVWA 分级绕过实战
前言
今天继续 DVWA Web 漏洞靶场系列,来学习另一个 Web 安全中出现频率极高、影响范围极广的经典漏洞——Reflected Cross Site Scripting(反射型跨站脚本,简称 Reflected XSS)。
如果说 SQL 注入是让攻击者影响数据库查询逻辑,命令注入是让攻击者影响服务器操作系统,那么 XSS 的目标则是——让攻击者控制其他用户浏览器中的页面行为。
一旦 XSS 利用成功,攻击者注入的 JavaScript 代码会在受害者浏览器中,以目标网站的身份和权限运行。轻则弹窗、篡改页面内容,重则可能造成钓鱼、会话劫持、敏感信息读取、账户操作诱导等问题。
XSS 的核心原理同样很简单:
Web 应用将用户可控输入直接输出到 HTML 页面中,却没有进行正确的上下文编码,导致浏览器把输入内容当成 HTML 标签或 JavaScript 代码解析执行。
DVWA 的 Reflected XSS 靶场正好通过 Low、Medium、High、Impossible 四个安全等级,完整展示了“无过滤 → 黑名单过滤 → 正则过滤 → 输出编码”的防护演进过程。
1. Reflected XSS 核心理论基础
1.1 什么是反射型 XSS?
反射型跨站脚本漏洞(Reflected Cross Site Scripting),对应常见漏洞编号:
CWE-79:Improper Neutralization of Input During Web Page Generation(Web 页面生成过程中对输入未进行正确处理)
反射型 XSS 的典型特征是:
- 攻击者构造恶意参数;
- 参数通过 URL 查询字符串、表单提交等方式发送给服务器;
- 服务器将该参数“反射”回响应页面;
- 浏览器解析页面时执行攻击者注入的脚本;
- 恶意内容通常不会被永久保存到数据库中。
例如,某页面存在一个姓名参数:
http://dvwa.local/vulnerabilities/xss_r/?name=Alice
后端将参数直接拼接进页面:
echo "Hello " . $_GET['name'];
正常访问时,页面显示:
Hello Alice
但如果用户输入中包含可被浏览器解析的标签或事件处理代码,并且后端没有进行安全编码,浏览器就可能把它当作页面的一部分执行。
1.2 反射型 XSS 的本质
反射型 XSS 的漏洞链路可以概括为:
攻击者可控输入
↓
URL 参数 / 表单参数 / 请求头等
↓
后端未正确过滤或未进行输出编码
↓
输入被拼接到 HTML 响应页面
↓
浏览器将输入当作 HTML / JavaScript 解析
↓
恶意脚本在目标站点上下文中执行
最关键的问题不在于“用户输入了 <script>”,而在于:
服务器没有区分“普通文本”和“浏览器可执行的 HTML 内容”。
开发者原本希望页面显示的是一段文字,但浏览器最终接收到的却是一个真正的标签、属性或脚本片段。
1.3 反射型 XSS 成立的必要条件
并不是所有能回显输入的页面都会产生 XSS。通常需要同时满足以下几个条件。
条件一:攻击者能够控制某个输入点
常见输入来源包括:
- URL 查询参数,例如
?name=xxx - GET、POST 表单参数
- 搜索框内容
- 错误提示中的参数
- 跳转地址、回调参数
- HTTP 请求头中的 Referer、User-Agent 等
条件二:输入会被输出到页面中
例如:
echo $_GET['name'];
或者:
echo "<div>搜索结果:" . $_GET['keyword'] . "</div>";
只要用户输入最终出现在 HTML 响应内容中,就需要进一步判断输出位置和编码方式。
条件三:输出时没有进行正确的上下文编码
错误示例:
echo "<pre>Hello {$name}</pre>";
安全的文本输出应当进行 HTML 编码:
echo "<pre>Hello " . htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "</pre>";
经过 htmlspecialchars() 处理后,<、>、"、' 等特殊字符会被转换为 HTML 实体,浏览器只会把它们显示成普通文字,而不会当作标签或脚本解析。
1.4 Reflected XSS、Stored XSS 与 DOM XSS 的区别
XSS 并不只有一种类型,最常见的是以下三类:
| 类型 | 恶意内容是否持久化 | 触发方式 | 典型场景 |
|---|---|---|---|
| Reflected XSS(反射型) | 不持久化 | 用户访问恶意链接或提交恶意请求 | 搜索页面、报错页面、参数回显页面 |
| Stored XSS(存储型) | 持久化到数据库或文件 | 其他用户访问已被污染的页面 | 评论区、昵称、留言板、个人资料 |
| DOM XSS | 不一定经过后端 | 前端 JavaScript 直接操作 DOM | location.hash、前端模板拼接、innerHTML |
其中,反射型 XSS 常常需要攻击者诱导受害者点击一个精心构造的 URL,因此它也经常出现在钓鱼链接、伪造通知、社工攻击等场景中。
1.5 XSS 可能造成什么危害?
XSS 并不只是一个“弹窗漏洞”。
弹窗只是验证脚本是否执行成功的最直观方式,真实风险取决于页面权限、浏览器安全策略、Cookie 属性、站点功能以及受害者身份。
常见影响包括:
- 篡改页面内容,伪造登录框、支付提示或客服窗口;
- 诱导用户输入账号、密码、验证码等敏感信息;
- 读取页面中本来只有当前用户可见的数据;
- 以受害者身份发起页面内可执行的操作;
- 窃取未配置
HttpOnly的会话 Cookie; - 利用管理员访问的后台页面扩大影响范围;
- 配合 CSRF、点击劫持、钓鱼等攻击形成组合攻击链。
需要注意的是:
HttpOnlyCookie 可以降低 Cookie 被 JavaScript 直接读取的风险,但它不能修复 XSS 漏洞本身。攻击者依然可能通过页面篡改、操作诱导、读取 DOM 数据等方式造成影响。
2. DVWA Reflected XSS(Low)
2.1 页面黑盒探测

首先在输入框中提交正常文本:
Alice
观察页面是否将输入内容原样显示。

页面显示:
Hello Alice
说明当前输入会被后端接收并回显到 HTML 响应中。
下一步,需要判断应用是将用户输入作为普通文本显示,还是直接交给浏览器解析。
在本地 DVWA 授权靶场中,可使用一个无害的验证 Payload:
<script>alert(document.domain)</script>
如果浏览器弹出当前站点域名,说明输入中的 <script> 标签没有被编码,浏览器已经把用户输入当成脚本执行。

2.2 漏洞利用原理
Low 级别的核心问题是:
用户输入没有经过任何过滤、转义或编码,直接被拼接到 HTML 页面中。
攻击者提交的内容:
<script>alert(document.domain)</script>
后端返回页面时,可能形成类似结构:
<pre>Hello <script>alert(document.domain)</script></pre>
浏览器在解析 HTML 时,不会把 <script> 当成普通文本,而是会把它识别为真正的脚本标签,因此 JavaScript 得以执行。
需要特别注意:
<pre>
标签只能保留文本格式和空白字符,并不能阻止内部 HTML 标签被解析。
很多开发者误以为把用户输入放进 <pre>、<div>、<p> 等普通标签中就安全了,但只要没有进行输出编码,浏览器仍会解析用户输入里的标签。
2.3 源码审计:无过滤直接回显
DVWA Low 级别的核心逻辑通常类似于:
<?php
header ("X-XSS-Protection: 0");
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Feedback for end user
echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}
?>
逐行解读:
header ("X-XSS-Protection: 0");
主动关闭浏览器自带的XSS防护,避免浏览器拦截测试Payload,方便漏洞复现。- 判断逻辑
仅校验GET请求里name参数是否存在、内容不为空,不存在任何过滤、字符替换、HTML转义操作,用户传入的内容会原样获取。 - 漏洞关键语句
echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
程序直接将用户可控的name参数拼接进HTML代码中输出。
即便内容被包裹在<pre>标签内部,<pre>仅用于保留文本空格与换行,不会阻止浏览器解析<script>这类HTML标签。
最终页面源码会拼接为:
<pre>Hello <script>alert(document.domain)</script></pre>
浏览器解析页面时识别script标签并执行JS代码,反射型XSS成功触发。
漏洞根源概括
用户可控输入未经HTML实体编码、无任何过滤,直接嵌入HTML页面返回客户端,浏览器将恶意内容当作脚本解析运行。
标准修复方式
使用htmlspecialchars()对输出内容转义,把< > & " '等特殊字符转为HTML实体,浏览器只会视作纯文本展示:
$name = htmlspecialchars($_GET['name'], ENT_QUOTES);
echo '<pre>Hello ' . $name . '</pre>';
3. DVWA Reflected XSS(Medium)
3.1 页面黑盒探测
Medium 级别通常会尝试过滤最常见的 <script> 标签。
当直接提交:
<script>alert(document.domain)</script>
会发现页面不再弹窗

这说明应用程序已经加入了一层过滤逻辑。
但这里必须明确:
“过滤了某一种 Payload”不等于“修复了 XSS 漏洞”。
如果防护逻辑只是删除固定字符串,例如删除小写的 <script> 和 </script>,那么攻击者仍然可以通过大小写变化、其他 HTML 标签、事件属性等方式绕过。
3.2 黑名单绕过思路
假设后端只过滤了:
<script>
</script>
那么大小写混合的标签就可能绕过检查:
<ScRiPt>alert(document.domain)</ScRiPt>
这里我们可以看到

原因在于 HTML 标签名通常不区分大小写,但 PHP 的普通字符串替换函数默认是区分大小写的。
也就是说:
<script>
和:
<ScRiPt>
对于浏览器来说都可能是脚本标签;但对于简单字符串黑名单来说,它们并不一定相同。
除了 <script> 标签外,浏览器中还存在大量可触发脚本执行的上下文,例如带有事件属性的元素:
<img src=x onerror=alert(document.domain)>
当图片加载失败时,浏览器可能触发 onerror 中的 JavaScript。
这也说明了黑名单方案的根本问题:
需要拦截的不是一个单词,而是浏览器中大量可能进入脚本执行上下文的语法组合。
黑名单很难覆盖完整,漏掉一个入口就可能被绕过。
3.3 源码审计:简单字符串替换的缺陷
Medium 级别后端PHP源码:
<?php
header ("X-XSS-Protection: 0");
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = str_replace( '<script>', '', $_GET[ 'name' ] );
// Feedback for end user
echo "<pre>Hello {$name}</pre>";
}
?>
代码逻辑拆解
页面依旧接收GET方式传入的name参数,相比Low级别新增了一处防护:使用str_replace('<script>', '', 变量),把内容里完整的小写<script>字符串直接清空删除,本意是屏蔽脚本标签,阻止<script>alert()</script>这类基础Payload执行。
其余逻辑没有改动:依旧关闭浏览器XSS防护、无HTML转义处理,替换完成后的内容直接拼接进HTML页面回显。
防护存在的致命缺陷
str_replace属于纯文本字面替换函数,只会机械检索固定字符串进行删除,不会解析HTML语法、不区分标签大小写、不识别浏览器的HTML容错解析规则,属于简陋的黑名单防御,漏洞依旧可以绕过。
常见绕过Payload原理
- 大小写混合绕过
str_replace仅匹配小写<script>,大小写混杂后不会被识别替换
Payload:<ScRiPt>alert(document.domain)</ScRiPt>
字符串检索不到<script>,替换失效,浏览器不区分HTML标签大小写,脚本正常执行。 - 嵌套标签绕过
Payload:<scr<script>ipt>alert(1)</scr</script>ipt>
运行过程:
函数先剔除中间的<script>,文本变为<script>alert(1)</script>,完整脚本标签重现,成功触发XSS。 - 舍弃script标签,使用事件属性标签
防御仅拦截了script标签,没有管控img、svg等携带事件触发的HTML标签:<img src=1 onerror=alert(document.domain)>``<svg onload=alert(1)>
这类载荷不含<script>,不会被替换删除,依靠加载事件执行JS,轻松绕过限制。
漏洞本质
依靠黑名单关键字删除的方式无法穷尽所有可执行JS的HTML写法,只要不对输出内容做HTML实体编码,特殊符号依旧会被浏览器解析为HTML语法,XSS漏洞无法彻底根除。
正确修复方案
摒弃黑名单替换,统一使用htmlspecialchars()进行输出转义,将< > & " '等HTML特殊字符转为实体字符,无论何种标签、大小写、事件属性,都会被渲染成纯文本,彻底杜绝反射型XSS:
$name = htmlspecialchars($_GET['name'], ENT_QUOTES);
echo "<pre>Hello {$name}</pre>";
4. DVWA Reflected XSS(High)
4.1 页面黑盒探测
High 级别一般会使用正则表达式检测 <script> 的变形,防护比 Medium 更严格。
例如,大小写混合的 <ScRiPt> 已经无法绕过。

此时说明防护逻辑不再是简单匹配固定字符串,而是开始尝试识别脚本标签的不同写法。
但 High 级别依旧存在一个核心问题:
它的防护目标仍然是“识别并删除危险标签”,而不是“让用户输入以普通文本方式安全显示”。
如果开发者只是把检测范围从字符串替换升级成正则表达式,依然无法真正覆盖全部 HTML 解析场景。
4.2 黑名单与正则过滤为什么仍然不可靠?
假设后端通过正则表达式尝试删除所有疑似 <script> 标签:
preg_replace('/<.*s.*c.*r.*i.*p.*t.*>/i', '', $name);
这种写法看起来比简单的 str_replace() 更严格,但仍然存在明显问题。
第一,它只关注 <script> 标签。
而 XSS 并不只依赖 <script> 标签,许多 HTML 标签及事件属性同样可能触发脚本执行。
第二,正则表达式并不适合完整解析 HTML。
HTML 的标签、属性、实体编码、浏览器容错机制、不同上下文嵌套关系都非常复杂。试图用一条正则表达式覆盖所有危险输入,最终通常会出现:
- 该拦截的没拦住;
- 正常内容被误杀;
- 后续维护困难;
- 新的浏览器解析差异无法及时覆盖。
第三,黑名单思路本身就是被动防御。
开发者需要先知道所有危险写法,再逐一拦截;攻击者只需要找到一个没有被考虑到的语法入口即可。
所以 High 级别的核心教训是:
黑名单从“字符串过滤”升级到“正则过滤”,并没有改变防护方向错误的问题。
4.3 黑盒绕过测试与Payload
绕过核心:彻底舍弃script标签,使用HTML事件属性触发JavaScript,载荷不含script字段,避开正则匹配规则。
- 首选Payload:img标签加载错误事件
<img src=x onerror=alert(document.domain)>
原理:src赋值无效地址,图片加载失败触发onerror事件,执行弹窗代码,无script内容,正则不会过滤,页面加载后成功执行XSS。 - svg加载事件Payload
<svg onload=alert(document.cookie)>
页面渲染完成后自动执行JS脚本,可用于窃取Cookie。 - input自动聚焦触发
<input onfocus=alert(1) autofocus>
4.4 源码审计:正则过滤不等于安全编码
High级别后端完整PHP源码:
<?php
header ("X-XSS-Protection: 0");
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] );
// Feedback for end user
echo "<pre>Hello {$name}</pre>";
}
?>
代码解析
header ("X-XSS-Protection: 0");关闭浏览器自带XSS防护,方便漏洞测试;- 获取GET参数name,仅校验参数是否存在、非空;
- 正则忽略大小写模糊匹配script字符串,所有包含script的标签都会被清空;
- 处理完毕的变量直接拼接进HTML页面输出,全程未进行HTML实体转义。
漏洞根源
Low、Medium、High三级均使用黑名单防御机制,区别仅在于过滤严苛程度。只拦截危险标签,不在输出时调用htmlspecialchars()转义特殊字符,用户可控内容始终会被浏览器解析为HTML,无法彻底抵御XSS攻击。
修复方案
<?php
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
$name = htmlspecialchars($_GET['name'], ENT_QUOTES);
echo "<pre>Hello {$name}</pre>";
}
?>
对输出内容进行HTML编码,< > & " '全部转为实体字符,所有HTML标签都会以文本形式展示,XSS漏洞彻底修复。
5. DVWA Reflected XSS(Impossible)
5.1 源码审计
Impossible 等级摒弃黑名单过滤思路,采用HTML实体编码输出 + CSRF令牌校验双重安全机制,从根源防御反射型XSS,后端完整源码如下:
<?php
// 判断GET请求是否携带name参数且参数不为空
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// 校验CSRF防护令牌
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// 获取用户输入,并进行HTML实体编码
$name = htmlspecialchars( $_GET[ 'name' ] );
// 将处理后的内容回显至页面
echo "<pre>Hello {$name}</pre>";
}
// 生成全新的会话CSRF令牌
generateSessionToken();
?>
代码分段解析
- Anti-CSRF 令牌校验
checkToken()会核对请求携带的令牌与服务端会话存储令牌是否一致;
每次请求结束调用generateSessionToken()刷新令牌,令牌单次有效。
攻击者无法伪造合法token构造恶意链接,大幅削减反射型XSS的利用条件。 - 核心防护函数:htmlspecialchars()
该函数会剥夺特殊字符的HTML语法功能,将其转换为纯文本实体:
| 原始字符 | 转换后HTML实体 |
|---|---|
< |
< |
> |
> |
" |
" |
& |
& |
注:默认不会转义单引号
',传入参数ENT_QUOTES才可将单引号转为'
编码效果演示
用户提交恶意载荷:<script>alert(document.domain)</script>
经过函数转义后,页面最终源码:<script>alert(document.domain)</script>
浏览器渲染效果:
纯文本展示:<script>alert(document.domain)</script>
不会识别为脚本标签,JS代码无法执行,XSS攻击失效。
5.2 为什么输出编码才是正确修复方式?
旧方案(Low/Medium/High:黑名单拦截)
逻辑流程:
预判所有危险字符/标签 → 字符串替换/正则匹配 → 发现后删除过滤
弊端:
HTML可执行JS的方式不计其数,无法穷举全部攻击载荷;规则永远滞后于攻击手法,漏洞始终存在绕过空间。
正确方案(Impossible:输出HTML编码)
逻辑流程:
不区分输入好坏,所有用户可控内容统一转义 → 全部以普通文本形式输出
核心原理:< > & " 等字符失去HTML标签、事件属性的语法意义,浏览器只会将内容当作文字解析,无需持续增补过滤规则,从底层阻断XSS触发条件。
5.3 Impossible 级别安全总结
- 彻底舍弃黑名单防御体系
不再依靠正则、字符串替换拦截恶意标签与事件载荷,规避黑名单覆盖不全的固有缺陷; - 输出点统一执行HTML实体编码
利用htmlspecialchars()消解特殊字符的HTML语义,杜绝浏览器解析恶意脚本; - 新增CSRF一次性令牌防护
限制恶意链接批量传播利用,收缩反射型XSS攻击场景; - 可拓展优化
添加ENT_QUOTES参数可同时转义单、双引号,适配标签属性内回显场景,防护更加完备。
6. XSS 防护的正确姿势
6.1 第一原则:根据输出上下文进行编码
不同输出位置需要不同的编码策略。
| 输出位置 | 推荐防护方式 |
|---|---|
| HTML 文本节点 | HTML 实体编码,如 htmlspecialchars() |
| HTML 属性值 | 属性编码,并始终使用引号包裹属性值 |
| JavaScript 字符串 | JavaScript 上下文编码,避免直接字符串拼接 |
| URL 参数 | URL 编码,如 urlencode() |
| CSS 上下文 | CSS 编码,尽量避免拼接用户输入 |
必须牢记:
输入过滤不能替代输出编码。
用户输入可能经过多个业务流程,最终出现在不同页面、不同上下文中。真正靠近风险点的位置,是“输出到浏览器之前”。
6.2 不要使用黑名单修复 XSS
以下写法都不推荐作为 XSS 的修复手段:
str_replace('<script>', '', $input);
preg_replace('/script/i', '', $input);
str_replace('onerror', '', $input);
原因是黑名单无法穷尽浏览器可以解析的所有危险语法。
开发者越试图维护“危险关键字列表”,越容易陷入规则膨胀、漏拦截和误拦截的问题。
6.3 必须支持富文本时,使用白名单净化方案
有些业务确实需要用户提交富文本,例如文章发布、评论编辑器、知识库系统。
此时不能简单使用 htmlspecialchars() 把所有标签都转义,否则格式会全部丢失。
正确思路是:
明确允许哪些标签、哪些属性、哪些协议
↓
删除其他所有内容
↓
使用经过安全验证的 HTML 净化库处理
例如,只允许:
p、br、strong、em、ul、ol、li、a
并且限制链接协议只能是:
http、https
不要自己用正则表达式“过滤 HTML”,应使用成熟且经过长期验证的 HTML Sanitizer / Purifier 类库。
6.4 CSP 可以降低风险,但不能代替修复漏洞
内容安全策略(Content Security Policy,CSP)可以帮助浏览器限制脚本加载和执行来源。
例如,可以限制页面只允许加载本站脚本,禁止内联脚本等。
但必须注意:
CSP 是纵深防御措施,不是 XSS 漏洞的替代修复方案。
正确顺序应该是:
优先修复输出编码问题
↓
再通过 CSP 降低残余风险
↓
配合 HttpOnly、Secure、SameSite Cookie 等安全配置
7. DVWA Reflected XSS 四个等级对比
| 安全等级 | 核心防护逻辑 | 防护效果 | 主要问题 |
|---|---|---|---|
| Low | 不做过滤,直接回显 | 完全可利用 | 用户输入直接进入 HTML |
| Medium | 过滤固定 <script> 字符串 |
容易绕过 | 大小写、事件属性、其他标签未覆盖 |
| High | 正则匹配脚本标签 | 有一定拦截效果 | 本质仍是黑名单,无法完整解析 HTML |
| Impossible | htmlspecialchars() 输出编码 |
可有效阻断当前反射型 XSS | 正确的上下文编码思路 |
从 DVWA 的四个等级可以非常直观地看出防护演进:
无防护
↓
简单黑名单
↓
复杂黑名单
↓
输出编码
真正可靠的安全方案,不是把黑名单写得越来越长,而是:
不要让不可信输入以浏览器可执行语法的形式进入页面。
8. 总结
反射型 XSS 的漏洞原理并不复杂:
用户输入被服务端反射到页面中,且没有经过正确的输出编码,浏览器就可能把输入当成 HTML 或 JavaScript 执行。
DVWA 的不同安全等级分别展示了:
- Low:直接回显,完全无防护;
- Medium:简单字符串黑名单,容易绕过;
- High:正则黑名单,仍然无法覆盖全部 XSS 场景;
- Impossible:使用
htmlspecialchars()进行 HTML 输出编码,从根源阻断标签解析。
最后记住 XSS 防御最重要的一句话:
不要试图猜测攻击者会输入什么;要确保无论用户输入什么,都只能以普通文本的形式被浏览器展示。
免责声明
本文所有内容仅用于网络安全技术研究与教学演示,所有测试均在本地授权搭建的 DVWA 靶场环境中进行,旨在帮助读者理解反射型 XSS 漏洞原理与防御机制。
请勿将本文涉及的技术与方法用于任何未经授权的真实系统测试。任何利用本文内容进行的非法攻击行为,均与作者无关,由使用者自行承担相应法律责任。
网络安全学习与测试应严格遵守相关法律法规,仅在获得明确授权的前提下开展安全测试。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)