【缺陷周话】第12期:存储型 XSS
字数 2010 2025-08-18 11:37:45
存储型XSS漏洞分析与防御指南
1. 存储型XSS概述
存储型XSS(Persistent XSS)是指应用程序通过Web请求获取不可信赖的数据,在未检验数据是否存在XSS代码的情况下,便将其存入数据库。当下一次从数据库中获取该数据时程序也未对其进行过滤,页面再次执行XSS代码,从而形成持续攻击。
1.1 基本特征
- 攻击载荷存储在服务器端(数据库、文件系统等)
- 每次访问受影响页面都会触发攻击
- 影响范围广,可攻击所有访问该页面的用户
- 常见于留言板、评论区、用户资料等需要持久化存储用户输入的场景
1.2 与反射型XSS的区别
| 特征 | 存储型XSS | 反射型XSS |
|---|---|---|
| 存储位置 | 服务器端 | 不存储 |
| 触发方式 | 访问存储页面 | 点击恶意链接 |
| 影响范围 | 所有访问用户 | 仅点击链接的用户 |
| 持续时间 | 持久性 | 一次性 |
2. 存储型XSS的危害
存储型XSS可造成以下安全威胁:
- Cookie窃取:攻击者可获取用户会话凭证
- 页面劫持:篡改页面内容或重定向到恶意网站
- 键盘记录:记录用户输入敏感信息
- 传播恶意软件:利用浏览器漏洞传播恶意程序
- 钓鱼攻击:伪造登录表单窃取凭证
2.1 实际漏洞案例
| CVE编号 | 受影响系统 | 攻击方式 |
|---|---|---|
| CVE-2018-19178 | JEESNS 1.3 | 通过HTML EMBED元素攻击 |
| CVE-2018-19170 | JPress v1.0-rc.5 | 通过设置模块输入字段攻击 |
| CVE-2018-19089 | Tianti 2.3 | 通过用户角色名称参数攻击 |
| CVE-2018-17369 | EasyCMS 1.3 | 通过文章标题、内容等字段攻击 |
3. 漏洞代码分析
3.1 缺陷代码示例
// 从数据库获取用户数据
Connection dbConnection = DriverManager.getConnection("jdbc:mysql://localhost:3306/database", "user", "password");
Statement statement = dbConnection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT name FROM users WHERE id=0");
String data = resultSet.getString(1);
// 不充分的过滤
data = data.replaceAll("<script>", "");
// 直接输出到页面
response.getWriter().println("User name: " + data);
漏洞点分析:
- 从数据库获取数据后未进行充分验证
- 仅过滤
<script>标签,其他HTML标签和属性仍可造成XSS - 直接输出未编码的内容到HTML响应中
3.2 攻击场景
攻击者可提交包含恶意脚本的用户名,如:
该载荷将:
- 被存储到数据库
- 每次用户访问页面时执行
- 绕过简单的
<script>过滤
4. 修复方案
4.1 输入验证
对用户提交的数据进行严格验证:
// 使用白名单验证
if (!data.matches("[a-zA-Z0-9]+")) {
throw new IllegalArgumentException("Invalid input");
}
4.2 输出编码
使用ESAPI进行上下文相关的输出编码:
import org.owasp.esapi.ESAPI;
// HTML实体编码
String safeOutput = ESAPI.encoder().encodeForHTML(data);
response.getWriter().println("User name: " + safeOutput);
ESAPI提供的编码方法:
encodeForHTML()- HTML内容编码encodeForHTMLAttribute()- HTML属性编码encodeForJavaScript()- JavaScript上下文编码encodeForCSS()- CSS上下文编码encodeForURL()- URL参数编码
4.3 综合修复代码
// 获取数据
Connection dbConnection = DriverManager.getConnection("jdbc:mysql://localhost:3306/database", "user", "password");
Statement statement = dbConnection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT name FROM users WHERE id=0");
String data = resultSet.getString(1);
// 输出编码
String safeOutput = ESAPI.encoder().encodeForHTML(data);
response.getWriter().println("User name: " + safeOutput);
4.4 设置HttpOnly Cookie
Cookie cookie = new Cookie("sessionID", "123456789");
cookie.setHttpOnly(true);
response.addCookie(cookie);
5. 防御最佳实践
-
输入验证:
- 采用白名单而非黑名单策略
- 根据业务需求定义严格的输入规则
- 对特殊字符进行过滤或转义
-
输出编码:
- 根据输出上下文选择合适的编码方式
- 使用成熟的编码库而非自行实现
- 注意不同上下文的编码规则差异
-
安全配置:
- 设置HttpOnly和Secure标志的Cookie
- 实施Content Security Policy (CSP)
- 使用X-XSS-Protection响应头
-
安全开发:
- 将安全要求纳入开发规范
- 进行安全代码审查
- 使用静态分析工具检测漏洞
6. 检测与验证
6.1 手动测试方法
- 在输入字段尝试提交XSS测试向量:
<script>alert(1)</script> "><script>alert(1)</script> - 查看页面源代码确认是否被编码
- 访问其他页面验证攻击是否持续
6.2 自动化检测
使用工具如:
- OWASP ZAP
- Burp Suite
- 360代码卫士
- Fortify
- Checkmarx
7. 相关CWE条目
- CWE-79: Web页面生成期间输入中和不当(跨站脚本)
- CWE-80: Web页面中脚本相关HTML标签中和不当(基本XSS)
- CWE-81: 错误消息Web页面中脚本中和不当
- CWE-82: Web页面IMG标签属性中脚本中和不当
- CWE-83: Web页面属性中脚本中和不当
8. 总结
存储型XSS是Web应用中最危险的安全漏洞之一,其持久性特点使得攻击影响范围广、持续时间长。有效防御需要采取多层次的安全措施:
- 不信任任何用户输入 - 包括来自数据库的"已存储"数据
- 实施深度防御 - 结合输入验证、输出编码和安全配置
- 使用安全工具 - 借助自动化工具进行持续检测
- 提高安全意识 - 将安全融入开发生命周期
通过遵循这些原则和实践,可以显著降低存储型XSS漏洞的风险,保护用户和系统安全。