1. 问题背景与发现过程
上周在给客户部署若依Plus框架时,意外触发了系统内置XSS过滤器的异常行为。当时我们正在测试一个普通的富文本编辑器提交功能,当用户输入包含特定HTML标签组合时,后端竟然返回了500错误。经过层层排查,最终定位到是框架自带的XSSFilter组件在处理某些特殊字符序列时存在逻辑缺陷。
这个bug的隐蔽性在于,它不会在常规XSS攻击场景下暴露(比如简单的 ),而是在处理某些合法的HTML片段时错误地拦截了内容。更麻烦的是,错误信息被吞没在框架底层,前端只能收到模糊的"服务器内部错误"提示。
2. 漏洞原理深度解析
2.1 若依Plus的XSS防御机制
若依Plus基于Spring Boot的XSS防护方案主要包含三个层级:
- 前端使用vue-xss进行基础过滤
- 后端通过XSSFilter进行请求参数过滤
- 响应阶段通过Jackson的JSON序列化进行二次转义
问题出在第二层的XSSFilter实现上。其核心逻辑是对所有请求参数值进行HTML标签剥离,关键代码如下:
public String filter(String value) { if (value == null) return null; String cleanValue = StringEscapeUtils.escapeHtml4(value); cleanValue = Jsoup.clean(cleanValue, Safelist.relaxed()); return cleanValue; }2.2 具体缺陷分析
当遇到类似下面的HTML片段时就会触发异常:
<div class="chart-container" style="width:100%"> <svg width="100%" height="300px"> <rect x="10" y="10" width="90%" height="90%"/> </svg> </div>问题根源在于:
- StringEscapeUtils.escapeHtml4() 会将所有特殊字符转为HTML实体
- Jsoup.clean() 处理时遇到"90%"这样的CSS百分比值
- 多层转义导致解析器状态混乱,最终抛出IllegalArgumentException
2.3 影响范围评估
该缺陷影响所有使用若依Plus 3.6.0以下版本的项目,特别是:
- 包含富文本编辑功能的模块
- 需要处理SVG图形的系统
- 使用CSS百分比布局的管理后台
- 需要提交复杂HTML片段的工单系统
3. 解决方案与修复步骤
3.1 临时解决方案
对于无法立即升级的项目,建议在Filter中添加白名单机制:
// 添加对特定路径的豁免 String uri = ((HttpServletRequest) request).getRequestURI(); if (uri.startsWith("/editor/save")) { chain.doFilter(request, response); return; }3.2 永久修复方案
官方在3.6.0版本中重构了过滤逻辑:
- 移除了冗余的StringEscapeUtils转义
- 改用更安全的Jsoup配置:
Safelist safelist = Safelist.relaxed() .addAttributes(":all", "style") .addProtocols("img", "src", "data"); Jsoup.clean(input, "", safelist, new OutputSettings().prettyPrint(false));- 添加了异常捕获机制:
try { return Jsoup.clean(value, safelist); } catch (Exception e) { log.warn("XSS过滤异常: {}", e.getMessage()); return value.replace("<", "<").replace(">", ">"); }3.3 升级操作步骤
- 修改pom.xml中的版本号:
<ruoyi.version>3.6.0</ruoyi.version>- 清理并重新编译项目:
mvn clean package -DskipTests- 特别检查以下配置文件:
- application.yml中的xss配置项
- 自定义的XSS过滤规则
- 富文本编辑器的白名单配置
4. 验证与测试方案
4.1 测试用例设计
建议构造以下测试场景:
- 包含数学公式的HTML:
<div>当x<y时成立</div>- SVG矢量图形:
<svg><circle cx="50%" cy="50%" r="40%"/></svg>- 复杂CSS样式:
<div style="width:calc(100% - 20px)"></div>4.2 自动化测试脚本
使用TestNG编写验证用例:
@Test public void testXssFilterWithSpecialChars() { String input = "<div>1 < 2 && 3 > 1</div>"; String output = xssFilter.filter(input); Assert.assertFalse(output.contains("<")); Assert.assertTrue(output.contains("<div>")); } @Test public void testSvgHandling() { String input = "<svg width='100%'><rect x='10%'/></svg>"; String output = xssFilter.filter(input); Assert.assertTrue(output.contains("svg")); }5. 深度防御建议
5.1 防御层优化方案
建议采用分层防御策略:
| 防御层 | 实现方式 | 防护目标 |
|---|---|---|
| 前端 | DOMPurify + 内容安全策略(CSP) | 即时反馈 |
| 网关 | ModSecurity规则 | 阻断已知攻击模式 |
| 后端 | 定制化XSSFilter | 业务逻辑防护 |
| 持久层 | 存储前二次校验 | 数据一致性 |
5.2 关键配置参数
在application.yml中建议配置:
xss: enabled: true exclude-urls: - /api/editor/save - /chart/data level: medium # [low|medium|high] allow-svg: false5.3 监控与日志
添加专门的审计日志:
@Aspect public class XssAuditLog { @AfterReturning("execution(* com.ruoyi.framework.web.filter.XssFilter..*(..))") public void logFilterOperation(JoinPoint jp) { String param = (String) jp.getArgs()[0]; if(param != null && param.length() > 1000) { log.warn("Large content filtered: {}", param.substring(0, 100)); } } }6. 经验总结与避坑指南
在实际项目中,我们总结出以下经验教训:
转义顺序陷阱:
- 错误做法:先HTML转义再JS转义
- 正确顺序:先解析再统一转义
性能优化点:
- 对已知安全路径添加跳过逻辑
- 对大文件上传接口禁用过滤
- 使用缓存已验证的安全内容
典型误判场景:
- 数学公式中的比较运算符
- 代码片段展示功能
- 国际化内容中的特殊符号
应急处理方案:
// 在全局异常处理器中添加 @ExceptionHandler(IllegalArgumentException.class) public R handleXssException(IllegalArgumentException e) { if (e.getMessage().contains("jsoup")) { return R.error("内容包含不安全字符"); } return R.error("参数错误"); }这个案例给我们的核心启示是:安全过滤组件的实现需要同时考虑防护有效性和内容兼容性。特别是在现代Web应用中,需要谨慎处理各类合法HTML5和CSS3特性,避免因过度防御导致业务功能异常。建议所有使用若依Plus的团队都检查自己的XSS过滤策略,确保不会拦截合法的内容输入。