JasperReports-代码审计报告

60次阅读
没有评论

JasperReports Library 代码安全审计报告

审计对象: JasperReports® Library(Jaspersoft 开源 Java 报表引擎)
代码路径: /jasperreports
版本: master @ a93869a1a,git describe = 7.0.8-49(7.0.8 之后 49 个提交,2026-08-24)
审计时间: 2026-09-09
审计方式: 静态代码审计 + 官方配置文档核对 + 公开漏洞库(NVD / GHSA / GitLab Advisory)交叉验证,未进行动态利用验证
审计范围: core/ 核心引擎 + ext/ 全部扩展模块;重点覆盖反序列化、表达式/脚本执行、资源加载(SSRF/LFI)、XML 解析(XXE)、JNDI、依赖组件
审计目的: 判断是否存在可导致系统权限/数据、用户权限/数据、应用权限/数据被获取或篡改的漏洞

⚠️ 前置声明: 本报告为授权范围内的防御性代码审计,仅用于安全加固参考。JasperReports 的信任边界是"报表模板"——官方设计即认为模板由开发者编写、可信;因此本报告严格区分两种场景:

  • 场景 A(不可信模板): 应用接收/编译外部上传的 .jrxml / .jasper → 等价于任意代码执行,这是设计属性而非实现缺陷;
  • 场景 B(模板可信、仅数据不可信): 模板由开发者维护,用户只提供参数/数据 → 此时需关注 SSRF/LFI、反序列化、XXE 等"数据面"漏洞。
    大多数真实系统的风险介于两者之间(如"低权限用户可上传模板""模板来自不可信仓库")。

一、审计结论摘要

1.1 已知 CVE 核对(本版本是否受影响)

CVE / GHSA 类型 影响版本 修复版本 本版本(7.0.8-49) 结论
CVE-2025-10492<br>GHSA-7c3f-cg9x-f3gr Java 反序列化 → RCE(CWE-502)<br>CVSS 3.1 = 9.8 / 4.0 = 8.7 < 7.0.4 7.0.4 已含修复代码 不受影响
CVE-2026-6009<br>GHSA-9wxq-mwqw-8hhg Java 反序列化 → RCE(CWE-502)<br>CVSS 4.0 = 8.7 < 7.0.7 7.0.7 已含修复代码 不受影响

核对依据(代码内可见修复):

  • core/.../engine/util/FilteredObjectInputStream.java — 重写 resolveClass(),对每个反序列化的类名调用过滤器(第 86–108 行);
  • core/.../engine/util/DeserializationClassFilter.java — 类白名单过滤器,属性 sinceVersion = VERSION_7_0_4,默认开启(第 39–47 行);
  • core/src/main/resources/default.jasperreports.properties:408 — net.sf.jasperreports.deserialization.class.filter.enabled=true;
  • 本地 git 历史包含修复提交且均为 HEAD 祖先:827c2f27c(add deserialization class filter)、3541a3e2b(add more classes to deserialization whitelist)、14c9ff647(separate class filters by purpose)、10f3862b2(filter classes on value deserialization)、4efaa3da5(ElementsBlock deserialization)、e3e775ad7(crosstab deserialization)、e7766f78f(repository URL filter,7.0.7 分支)。

注:同厂商 JasperReports Server 产品的未认证 XXE(CVE-2026-16626)属另一个产品,不在本仓库范围内,本报告未做代码验证。

1.2 代码级发现汇总

# 发现 级别 关键位置 触发条件
1 不可信 .jrxml 编译执行 → RCE 🔴 严重 JasperCompileManager → JRJdtCompiler → JRClassLoader.loadClassFromBytes 应用编译/填充不可信模板
2 不可信 .jasper 反序列化 + 内嵌字节码执行 → RCE 🔴 严重 JRLoader → CompiledClasses → JRAbstractJavaCompiler.loadEvaluator 应用加载不可信编译后模板(白名单挡不住)
3 表达式/脚本沙箱默认关闭(Java/Groovy/JavaScript) 🟠 高 ReportClassFilter、JRGroovyCompiler、ReportClassShutter 模板可控 + 未开启安全开关
4 资源加载无 URL 白名单 → SSRF / 本地文件读取 🟠 高 DefaultRepositoryService、JRFillSubreport、HttpDataService、JRCsvDataSource 模板可信但参数/路径可控
5 反序列化白名单过宽 + 无字节数上限 🟡 中 default.jasperreports.properties:409-462、FilteredObjectInputStream 接受外部序列化数据
6 XXE:核心已加固,Batik SVG / XSLT 待验证 🟡 中 JRXmlUtils(已加固)、SvgFontProcessor、QRCodeSVGImageProducer 处理外部 SVG / 需动态验证
7 JNDI 数据适配器 🟡 中 JndiDataAdapterService:72-73 数据源配置可控 + 旧 JDK
8 模板驱动的类加载/实例化(风险放大器) 🟡 中 DefaultFormatFactory:246-252、scriptlet 机制 模板可控 + 类在 classpath
9 依赖组件老化 / 里程碑版本 🟡 中 pom-parent.xml:122-145 供应链风险

总体结论:

  1. 代码本身未发现"仅靠不可信数据"即可打通的未授权 RCE;两个公开反序列化 CVE 在本版本均已修复。
  2. 但 JasperReports 的安全性完全取决于模板是否可信。只要能控制 .jrxml 或 .jasper,攻击者即可获得任意代码执行——这不是 bug,而是"模板即代码"的设计结果。
  3. 更值得注意的是:7.0.7 / 7.0.8 新增的两个安全缓解开关默认都是关闭的:
    • net.sf.jasperreports.report.class.filter.enabled=false(表达式类白名单 / 沙箱)
    • net.sf.jasperreports.repository.url.filter.enabled=false(资源 URL 白名单)
      即:默认配置下,报表对表达式和资源加载没有任何限制。

1.3 按漏洞类型的精确结论(纯代码验证,非检索结果)

判定口径:「数据可控」 = 模板可信、仅参数/字段/资源位置值来自不可信输入即可触发;「模板可控」 = 需要能修改/上传 .jrxml、.jasper 或报告属性;「配置可控」 = 需要能控制 JasperReports 配置/数据源配置。下表的每一行都能在本地源码中追到调用链(详见 2.11–2.13)。

漏洞类型 是否存在 精确结论 数据可控? 关键证据
反序列化 → RCE ✅ 存在 .jasper 是 Java 序列化对象,加载即 readObject;且其内嵌表达式字节码会在填充时 defineClass 执行。反序列化类白名单只挡 gadget 链,挡不住内嵌字节码 ⚠️ 当模板把字段/参数用作子报表位置时为数据可控 JRLoader.java:138/182/228、CompiledClasses.java:36-62、JRAbstractJavaCompiler.java:107-127、JRClassLoader.java:303-360
值反序列化 → RCE ✅ 存在(已被窄白名单缓解) 字段内容(markup="styled")中的 <a><param valueClass="..." >base64</param></a> 会触发 ObjectInputStream.readObject ✅ 是(字段数据直接触发) JRStyledTextParser.java:938-957 → JRValueStringUtils.java:132-145,556-576 → ValueClassFilter
命令执行 / RCE ✅ 存在(表达式/字节码执行) 引擎内没有可由数据触发的 Runtime.exec(JRJavacCompiler:63 仅非默认编译器、exec(String[]) 无 shell);RCE 来自表达式编译与内嵌字节码 模板可控直接 RCE;数据可控需经上面的 .jasper/子报表链 JRAbstractJavaCompiler.java:107-127、JRClassLoader.java:303-360
SQL 注入 ❌ 数据面不存在 JDBC $P{} 被替换为 ? 并由 PreparedStatement.setObject 绑定;Hibernate $P{} 替换为 :name。$P!{} 是显式原文注入(query.parameter.clause.enabled=true 默认开启),属模板编写问题 ❌(除非模板使用 $P!{}) JRJdbcQueryExecuter.java:285-288,404,771,803、JRHibernateQueryExecuter.java:357-365、JRAbstractQueryExecuter.java:495-521
MDX / XPath / XMLA 注入 ✅ 存在 这三种语言的 $P{} 不做参数化,getParameterReplacement 直接返回 String.valueOf(值) → 参数值原文进入查询 ✅ 是 JRMondrianQueryExecuter.java:90-93、JRXPathQueryExecuter.java:94-97、JRXmlaQueryExecuter.java:123-126
SSRF / 本地文件读取 ✅ 存在 子报表、图片、JSON/CSV/XML 数据源、HTTP 数据适配器、XMLA 均接受任意 URL/路径;repository.url.filter.enabled=false 默认关闭、default.file.repository.enabled=true 默认允许 file: ✅ 当模板把字段/参数用作位置时 DefaultRepositoryService.java:170-223、JRFillSubreport.java:365-430,459-482、JRFillImage.java:473-526、HttpDataService.java:571-612、JRXmlaQueryExecuter.java:158-186
Jackson 多态实例化 ✅ 存在(受类型约束) 数据适配器 @JsonTypeInfo(use=Id.CLASS),Jackson 未设 PolymorphicTypeValidator;可实例化 classpath 上任意 DataAdapter 实现并注入属性(如 JDBC URL → SSRF / JNDI 名称) ⚠️ 需控制 net.sf.jasperreports.data.adapter 指向的资源 DataAdapter.java:33、JacksonDataAdapterPersistenceService.java:38-70、JacksonUtil.java:141-222、DataAdapterParameterContributorFactory.java:92-126
XXE ❌ 核心不存在 核心 XML 入口均默认禁用 DOCTYPE/外部实体;SVG 走 Batik,本仓库未显式加固,需实测 — JRXmlUtils.java:229-238、JRStyledTextParser.java:223-224、JacksonReportLoader.java:111-114、XmlDataSniffer.java:88-93
JNDI 注入 ⚠️ 条件性 java:comp/env/ 前缀限制远程 lookup;需配置可控 + 旧 JDK ❌(配置可控) JndiDataAdapterService.java:72-73

一句话结论:如果问题是"模板可信、只喂数据"——真正由数据直接打通的只有三处:① 字段内容经 styled-text 触发的值反序列化(默认被窄白名单挡住);② 模板把字段/参数用作子报表/图片/数据源位置时的 SSRF/LFI,其中子报表位置 → .jasper 反序列化 → 字节码执行 = 数据可控 RCE;③ MDX/XPath 的 $P{} 原文替换导致查询注入。除此之外,所有 RCE 都要求模板可控。


二、漏洞详情

2.1 🔴 发现一:不可信 .jrxml 模板 = 任意代码执行(设计级)

证据链(表达式如何变成可执行代码):

步骤 代码位置
解析 .jrxml(7.0 用 Jackson XML) core/.../engine/xml/JacksonReportLoader.java
编译模板,用 JDT 把表达式生成 Java 源码并编译为字节码 ext/jdt/.../JRJdtCompiler.java(默认编译器,JasperCompileManager.java:730)
填充时加载并实例化求值器类 core/.../engine/design/JRAbstractJavaCompiler.java:107-127
从字节码 defineClass 并执行 core/.../engine/util/JRClassLoader.java:303-360(loadClassFromBytes → defineClass)

JasperReports 的表达式语言就是 Java,因此模板中的:

<textFieldExpression><![CDATA[Runtime.getRuntime().exec("id")]]></textFieldExpression>

在编译后即为普通 Java 调用,填充报表时执行。

影响:模板可控 → 系统命令执行、文件读写、内网访问(继承应用进程权限)。

边界说明:

  • 这是 JasperReports 的既定设计(表达式需要完整 Java 能力),官方立场是"不要加载不可信模板"。
  • 代码中确实提供了对应缓解开关 net.sf.jasperreports.report.class.filter.enabled(见 2.3),但默认关闭。

2.2 🔴 发现二:不可信 .jasper 即使开启反序列化白名单仍可 RCE

这是本次审计最重要的发现:CVE-2025-10492 / CVE-2026-6009 的修复解决了"gadget 链"路径,但无法阻止"内嵌字节码"路径。

原理:

  1. .jasper 是 Java 序列化对象(JasperReport),加载入口 JRLoader.loadObject() 直接 readObject():
    • core/.../engine/util/JRLoader.java:138 / 182 / 228
    • 通过 ContextClassLoaderObjectInputStream(core/.../engine/util/ContextClassLoaderObjectInputStream.java:55)套用类白名单过滤器。
  2. 序列化对象中保存了编译后的表达式字节码:
    • JasperReport.compileData(Serializable,JasperReport.java:74/140)
    • → JRReportCompileData(core/.../engine/design/JRReportCompileData.java:42)
    • → ReportExpressionEvaluationData → CompiledClasses(core/.../engine/design/CompiledClasses.java:36-62,本质是 Map<String, byte[]>)。
  3. 填充报表时,这些字节码被 defineClass 并实例化:
    • JRAbstractCompiler.createEvaluator() → loadEvaluator() → JRAbstractJavaCompiler.loadEvaluator() → JRClassLoader.loadClassFromBytes() → defineClass + newInstance。
  4. 为什么白名单放行:默认反序列化白名单包含
    • net.sf.jasperreports.engine.*、net.sf.jasperreports.engine.design.*、net.sf.jasperreports.compilers.*
      (core/src/main/resources/default.jasperreports.properties:420-462)
      → JasperReport、JRReportCompileData、CompiledClasses、java.util.HashMap、byte[] 全部在白名单内。

结论:攻击者构造的 .jasper 无需任何"gadget 类",只要在 CompiledClasses 中放入一段 <clinit>/构造器执行任意逻辑的字节码,即可在 fillReport() 时执行。类白名单对"字节码即 payload"无效。

缓解:

  • 开启 net.sf.jasperreports.report.class.filter.enabled=true 后,JRClassLoader 的类加载器过滤会拦截被定义类所引用的其他类(JRClassLoader.java:361-368 覆写 loadClass(name, resolve) 调用 classLoaderFilter.checkClassVisibility(name)),可阻断 java.lang.Runtime 等;但该开关默认关闭,且需按业务配置白名单。
  • 更根本的做法:服务端只加载自己编译/签名过的 .jasper。

2.3 🟠 发现三:表达式 / 脚本沙箱默认关闭

三个执行通道(Java 编译、Groovy、JavaScript)共用同一个过滤器 ReportClassFilter,而它默认关闭:

// core/src/main/java/net/sf/jasperreports/compilers/ReportClassFilter.java:44-52
@Property(category = PropertyConstants.CATEGORY_SECURITY,
          defaultValue = "false",          // ← 默认关闭
          sinceVersion = PropertyConstants.VERSION_6_13_0, ...)
public static final String PROPERTY_CLASS_FILTER_ENABLED =
        JRPropertiesUtil.PROPERTY_PREFIX + "report.class.filter.enabled";
# core/src/main/resources/default.jasperreports.properties:361
net.sf.jasperreports.report.class.filter.enabled=false

关闭时 AbstractClassFilter.isClassVisible() 对任何类名都返回 true:

// core/src/main/java/net/sf/jasperreports/engine/util/AbstractClassFilter.java:108-128
protected boolean visible(String className) {
    boolean visible;
    if (filterEnabled) { ...逐个白名单匹配... }
    else { visible = true; }   // ← 关闭即全放行
    return visible;
}

各执行通道的影响:

通道 位置 关闭时的后果
Java(默认 JDT 编译器) JRClassLoader + ReportClassFilter 生成的求值器类可引用任意 JDK/三方类
Groovy(ext/groovy) JRGroovyCompiler.java:94 仅在 reportClassFilter.isFilteringEnabled() 时添加 GroovyClassFilterTransformer(SandboxTransformer) Groovy 表达式不加沙箱,可直接调用任意类
JavaScript(ext/javascript,Rhino 1.8.1) JavaScriptEvaluatorScope.java:322-327 始终安装 ClassShutter,但 ReportClassShutter.java:47 委托给同一个 ReportClassFilter 过滤器关闭 → visibleToScripts() 恒为 true → JS 可访问 java.lang.Runtime 等

影响:与发现一叠加——模板可控时,Groovy/JS 表达式同样是一条直接 RCE 路径;即使只允许"受限语言",沙箱也未启用。


2.4 🟠 发现四:资源加载无 URL 白名单 → SSRF / 本地文件读取

7.0.7 新增了仓库 URL 过滤,但默认关闭,同时文件仓库默认开启:

# core/src/main/resources/default.jasperreports.properties:358-359
net.sf.jasperreports.default.file.repository.enabled=true
net.sf.jasperreports.repository.url.filter.enabled=false
// core/src/main/java/net/sf/jasperreports/repo/DefaultRepositoryService.java:74-82, 120-123
defaultValue = PropertyConstants.BOOLEAN_FALSE, sinceVersion = VERSION_7_0_7
...
this.urlWhitelistEnabled = JRPropertiesUtil.getInstance(jasperReportsContext)
        .getBooleanProperty(PROPERTY_URL_FILTER_ENABLED, false);

可达的加载点(模板可信、参数可控时即 SSRF/LFI):

加载点 位置 说明
子报表 core/.../engine/fill/JRFillSubreport.java:459-482 source 为 String → RepositoryUtil.getReport();为 URL/File/InputStream 时直接 JRLoader.loadObject
仓库资源 / URL 构造 core/.../engine/util/JRResourcesUtil.java:93-97 new URL(spec),支持任意 scheme(含 file:、jar:)
HTTP 数据适配器 ext/data-adapters-http/.../HttpDataService.java:571-612 URL 来自报告属性 net.sf.jasperreports.http.data.url 或参数 HTTP_DATA_URL,无主机/协议白名单
CSV 数据源 core/.../engine/data/JRCsvDataSource.java:160,172 url.openStream()
远程 XML 数据适配器 ext/data-adapters/.../xml/RemoteXmlDataAdapterService.java:138-140 new URL(fileName).openStream()

典型场景:应用使用固定模板,但把用户输入(如"报表名称""附件地址")作为子报表表达式、图片表达式或 HTTP 数据源 URL 的参数值 → 攻击者可让服务器请求内网服务/云元数据地址(SSRF),或读取本地文件(file:///etc/passwd,能否回显取决于报表是否展示内容)。

缓解:

  • 开启 net.sf.jasperreports.repository.url.filter.enabled=true 并配置 net.sf.jasperreports.repository.url.whitelist.* 正则白名单;
  • 在应用层校验所有进入报表的参数(协议 + 主机白名单,禁止 file:/jar:/内网 IP);
  • 渲染进程限制出网(egress policy)。

2.5 🟡 发现五:反序列化防护的残余风险

CVE 已修复,但防护属于"缓解"而非"根治":

  1. 白名单较宽(default.jasperreports.properties:409-419):
    java.util.HashMap、java.util.TreeMap、java.util.TreeSet、java.util.Hashtable、java.beans.PropertyChangeSupport、javax.swing.*、sun.util.calendar.ZoneInfo 等常见 gadget 基础类在白名单中;JasperReports 自身 engine.* / engine.design.* / compilers.* 整体放行(第 420–462 行)。
    → 白名单基于类名可见性,不检查 readObject/readResolve 行为,属于"缩小 gadget 面"。
  2. 类名解析走 TCCL:ContextClassLoaderObjectInputStream.resolveClass()(第 73–100 行)在父加载器失败时用线程上下文类加载器 Class.forName(name, false, contextClassLoader)——便于应用加载自身类,也意味着"白名单 + 类路径"共同决定可见类。
  3. 无字节数上限:net.sf.jasperreports.deserialization.byte.count.limit 默认 0(不限制,FilteredObjectInputStream.java:51-52,77)→ 超大序列化流可造成内存/CPU DoS。
  4. value 反序列化白名单默认为空(config.reference.xml:6078)——这一项配置是安全的。

建议:应用层额外注册 JDK 原生 ObjectInputFilter(ObjectInputStream#setObjectInputFilter),设置字节数上限,并拒绝接收外部 .jasper / .jrprint。


2.6 🟡 发现六:XML / XXE 防护现状

已加固(良好实践,可确认):

位置 措施
core/.../engine/util/JRXmlUtils.java:229-238 默认 disallow-doctype-decl;net.sf.jasperreports.xml.allow.doctype=false(default.jasperreports.properties:341)
core/.../engine/util/JRStyledTextParser.java:223-224 显式禁用 DOCTYPE
core/.../engine/fonts/SimpleFontExtensionHelper.java:146-148 显式禁用 DOCTYPE
core/.../engine/xml/JacksonReportLoader.java:111-114 IS_SUPPORTING_EXTERNAL_ENTITIES=false、IS_REPLACING_ENTITY_REFERENCES=false
core/.../renderers/util/XmlDataSniffer.java:88-93 禁用外部通用/参数实体与外部 DTD

待动态验证(中低置信):

位置 问题
core/.../renderers/util/SvgFontProcessor.java:77,98-100、SvgDataSniffer.java:59、AbstractSvgDataToGraphics2DRenderer.java 使用 Batik 1.19 的 SAXSVGDocumentFactory,代码中未见显式禁用外部实体 / 外部 DTD;SVG 输入若来自外部(图片、字体、用户内容)需实测 XXE/SSRF
ext/barcode4j/.../QRCodeSVGImageProducer.java:179、BarcodeSVGImageProducer.java:75 TransformerFactory.newInstance() 未设置 ACCESS_EXTERNAL_DTD / ACCESS_EXTERNAL_STYLESHEET(输入为内部生成的 SVG DOM,直接利用面较小)

验证建议(授权环境):向 SVG 处理路径投喂
<!DOCTYPE svg [<!ENTITY xxe SYSTEM "file:///etc/passwd">]> 及外部 DTD 引用,观察是否发起外部请求或回显文件内容。


2.7 🟡 发现七:JNDI 数据适配器

// ext/data-adapters/src/main/java/net/sf/jasperreports/data/jndi/JndiDataAdapterService.java:72-73
Context ctx = new InitialContext();
DataSource dataSource = (DataSource) ctx.lookup("java:comp/env/" + jndiDataAdapter.getDataSourceName());
  • 名称被固定前缀 java:comp/env/ 包裹,正常情况限制在本地命名空间,不能直接传入 ldap:///rmi:// 远程地址;
  • 但若应用把用户可控值拼进 dataSourceName(例如 ../../ldap://attacker/x),配合较旧 JDK 或未限制的 JNDI 提供者,仍存在 JNDI 注入风险;
  • 该模块需显式引入 ext/data-adapters 且配置 JNDI 数据源才可达。

建议:数据源名称使用固定枚举/白名单,禁止来自用户输入;JDK 保持最新并设置 com.sun.jndi.* 安全属性。


2.8 🟡 发现八:模板驱动的类加载 / 实例化(风险放大器)

以下机制本身不构成漏洞(需要类已在 classpath),但会放大"模板可控"的后果:

机制 位置
格式化工厂类按名加载并实例化 core/.../engine/util/DefaultFormatFactory.java:246-252(JRClassLoader.resolveClassForName + newInstance)
Scriptlet 类实例化 JRDataset.getScriptletClass() → 填充期实例化并回调
扩展注册(jasperreports_extension.properties) ExtensionsRegistry 机制按属性加载扩展类
Bean 属性反射访问 core/.../engine/data/JRAbstractBeanDataSource.java(commons-beanutils2 PropertyUtils,字段描述可控时可读取嵌套 getter)

2.9 🟡 发现九:依赖组件与供应链

来自 pom-parent.xml:118-145:

组件 版本 备注
batik 1.19 较新(历史 XXE/SSRF CVE 已修复,仍需 2.6 节验证)
castor 1.4.1 2011 年版本,ext/castor 已标注 @deprecated;XML unmarshalling 模块建议移除
commons-beanutils2 2.0.0-M2 里程碑版本,生产环境慎用
antlr 2.7.7 2006 年版本(ext/olap 构建期)
groovy 4.0.16 ext/groovy
groovy-sandbox 1.26-jaspersoft-2 默认不启用(见 2.3)
rhino 1.8.1 ext/javascript
jackson 2.18.8 较新
poi / log4j / hibernate / hsqldb 5.4.1 / 2.25.4 / 6.3.1 / 2.7.2(demo) 无异常

建议:项目自带 OWASP dependency-check 插件与 owasp-suppressions.xml,纳入 CI:
mvn org.owasp:dependency-check-maven:check


2.10 已检查且未发现问题的项

检查项 结论
SQL 注入(引擎自身) $P{param} 会转换为 ? 占位符由 PreparedStatement 绑定,参数不可注入;但 $P!{param} 是原始文本替换,模板可控时等同于 SQL 注入(属模板信任问题)
硬编码凭据 库中未发现硬编码账号/密钥(js-jrlib-ce_7.0.8_license.* 是许可证文件,非密钥)
未授权访问 本仓库是库而非服务,无自身 HTTP 端点;鉴权由集成方负责
目录穿越(模板加载) 由仓库服务路径拼接与 URL 过滤控制,见 2.4

2.11 🔴 数据可控反序列化链:子报表位置 → .jasper → 字节码执行

这是"模板可信、数据不可信"场景下唯一能到 RCE 的完整链路,逐步可验证:

模板(可信):  <subreportExpression>$F{REPORT_PATH}</subreportExpression>
        │
        ▼
JRFillSubreport.evaluateReportSource()      // core/.../fill/JRFillSubreport.java:365-369
        source = evaluateExpression(expression, evaluation);   // ← 字段/参数值(数据)
        ▼
JRFillSubreport.getReportSource()           // :372-441
        source instanceof String → RepositoryUtil.getResourceInfo()
        ▼
JRFillSubreport.loadReport()                // :459-482
        source instanceof URL    → JRLoader.loadObject(URL)      // :473
        source instanceof File   → JRLoader.loadObject(File)     // :477
        source instanceof String → RepositoryUtil.getReport(...) // :481
        ▼
JRLoader.loadObject()                       // core/.../util/JRLoader.java:138/182/228
        new ContextClassLoaderObjectInputStream(...).readObject()   // Java 反序列化
        ▼
JasperReport.compileData → JRReportCompileData → CompiledClasses  // Map<String,byte[]>
        ▼
填充时 JRAbstractCompiler.createEvaluator() → JRAbstractJavaCompiler.loadEvaluator()
        ▼
JRClassLoader.loadClassFromBytes() → defineClass() + newInstance()  // 字节码执行

为什么"白名单开着也挡不住":CompiledClasses、JasperReport、JRReportCompileData、java.util.HashMap、byte[] 都在默认反序列化白名单内(default.jasperreports.properties:409-462),而真正的执行点是填充阶段的 defineClass,与反序列化过滤器无关。.jasper 里的字节码本身就是合法 payload。

数据可控的前提:模板使用字段/参数作为子报表位置。这是常见写法(动态子报表),一旦应用的模板这么写、而字段值来自用户(数据库行、接口返回值、用户上传的附件路径),攻击者即可让服务器加载并执行任意 .jasper。

补充入口:应用直接调用 JRLoader.loadObject(userInput) / JRPrintXmlLoader.load(userInput)(预览、导入、断点续打等功能)时,同样命中。


2.12 🟠 数据可控 SSRF / 本地文件读取:位置类 sink 全清单

所有位置类 sink 最终都走 RepositoryUtil → DefaultRepositoryService.getInputStream() → JRResourcesUtil.createURL()(new URL(spec),支持 file:/http:/jar: 等任意已注册协议)→ url.openStream();而 URL 白名单默认关闭、文件仓库默认开启:

// core/src/main/java/net/sf/jasperreports/repo/DefaultRepositoryService.java:197-205
protected boolean isURLAllowed(URL url) {
    if (!urlWhitelistEnabled || urlWhitelist == null) {
        return true;                    // ← 默认(urlWhitelistEnabled=false)放行一切
    }
    ...
}
数据可控位置 加载代码 说明
子报表位置 JRFillSubreport.java:459-482 String/URL/File 三种类型都支持 → SSRF + 上面的反序列化链
图片位置 JRFillImage.java:473-526 → RendererUtil.getNonLazyRenderable(String) → RepositoryUtil.getBytesFromLocation() <imageExpression>$F{url}</imageExpression> → SSRF/本地文件读取(内容按图片解析)
JSON 数据源 ext/json/.../data/JsonDataSource.java:147-160 → JsonUtil.parseJson(ctx, location) 任意 URL/文件作为 JSON 数据源,内容进入报表 → 可回显文件内容
CSV 数据源 core/.../engine/data/JRCsvDataSource.java:215,234、:160,172 同上,file:///etc/passwd 可被当作 CSV 渲染出来
XML 数据源 core/.../engine/data/JRXmlDataSource.java:333 同上(DOCTYPE 已禁用,但内容可回显)
HTTP 数据适配器 ext/data-adapters-http/.../HttpDataService.java:571-612 URL 来自属性 net.sf.jasperreports.http.data.url 或参数 HTTP_DATA_URL,无主机/协议白名单
XMLA(OLAP) ext/olap/.../xmla/JRXmlaQueryExecuter.java:158-186 URL 来自报告参数 XMLA_URL → new URL(...) + SOAP 请求 → SSRF
远程 XML 数据适配器 ext/data-adapters/.../xml/RemoteXmlDataAdapterService.java:88,138-140 同上
字体/模板资源 AwtFontManager.java:94、XlsxZip.java:196、JRXlsExporter.java:304 等 位置多来自模板/配置属性

利用条件:模板把字段/参数用作上述位置值。加固开关:net.sf.jasperreports.repository.url.filter.enabled=true + net.sf.jasperreports.repository.url.whitelist.<name>=<regex>(DefaultRepositoryService.java:60-146)。


2.13 🟠/🟡 其余数据可控注入点(代码验证)

2.13.1 MDX / XPath / XMLA:$P{} 原文替换(默认行为)

// ext/olap/src/main/java/net/sf/jasperreports/olap/JRMondrianQueryExecuter.java:90-93
protected String getParameterReplacement(String parameterName) {
    return String.valueOf(getParameterValue(parameterName));   // ← 原文
}
// core/src/main/java/net/sf/jasperreports/engine/query/JRXPathQueryExecuter.java:94-97  同样原文
// ext/olap/src/main/java/net/sf/jasperreports/olap/xmla/JRXmlaQueryExecuter.java:123-126 同样原文

对比 JDBC / Hibernate(安全):

// JRJdbcQueryExecuter.java:285-288
protected String getParameterReplacement(String parameterName) { return "?"; }
// JRHibernateQueryExecuter.java:357-365
protected String getParameterReplacement(String parameterName) { return ':' + getHqlParameterName(parameterName); }

结论:JDBC/Hibernate 的 $P{} 不可注入;MDX/XPath/XMLA 的 $P{} 直接拼接。只要可信模板在这三种查询里使用 $P{param}(文档推荐写法),参数值(数据)即可注入查询语句 → 越权读取 OLAP/XML 数据(受数据源连接权限约束)。
另外 $P!{} 是所有语言的显式原文注入语法,由 net.sf.jasperreports.query.parameter.clause.enabled(默认 true,default.jasperreports.properties:206)控制(JRAbstractQueryExecuter.java:495-521)。

2.13.2 styled-text 超链接参数:字段数据触发 Java 反序列化

// core/src/main/java/net/sf/jasperreports/engine/util/JRStyledTextParser.java:938-957
if (nodeAttrs.getNamedItem(ATTRIBUTE_valueClass) != null) {
    parameter.setValueClass(nodeAttrs.getNamedItem(ATTRIBUTE_valueClass).getNodeValue());
}
String strValue = node.getTextContent();
if (strValue != null) {
    Object value = JRValueStringUtils.deserialize(parameter.getValueClass(), strValue);
    parameter.setValue(value);
}
  • 触发条件:报表字段设置 markup="styled"(或 html),字段内容包含 <a><param valueClass="…" >base64</param></a>。
  • 下游:JRValueStringUtils.java:556-576 → new FilteredObjectInputStream(context, bytesIn, new ValueClassFilter(context)).readObject()。
  • 当前缓解:ValueClassFilter 的白名单默认只有基本类型/日期/DisplayValue(default.jasperreports.properties:464-482),且过滤器默认开启。若应用为业务扩展了 value.deserialization.class.whitelist.*,或关闭 deserialization.class.filter.enabled,则字段数据即可反序列化 → RCE。
  • 同族入口:加载外部 .jrpxml 时的 GenericElementLoader.java:94、HyperlinkLoader.java:58。

2.13.3 数据适配器:Jackson Id.CLASS 实例化

// ext/data-adapters/src/main/java/net/sf/jasperreports/dataadapters/DataAdapter.java:33
@JsonTypeInfo(use=Id.CLASS, include=As.PROPERTY, property="class")
// 注释原文:we don't need a @JsonSubTypes annotation here because class name is going to be serialized
  • 加载链:报告/数据集属性 net.sf.jasperreports.data.adapter(DataAdapterParameterContributorFactory.java:92-117)→ RepositoryUtil.getResourceFromLocation(..., DataAdapterResource.class) → JacksonDataAdapterPersistenceService.load() → JacksonUtil.loadObject(json/xml, DataAdapter.class)。
  • JacksonUtil.configureMapper()(JacksonUtil.java:173-222)未设置 PolymorphicTypeValidator,Jackson 默认 LaissezFaireSubTypeValidator;Id.CLASS 允许按 JSON 中的 class 值实例化 classpath 上的任意 DataAdapter 实现(受"必须是 DataAdapter 子类型"约束),并可设置任意 Bean 属性。
  • 实际影响:可指定 JdbcDataAdapter.url 为任意 JDBC URL → 连接外部/内网(SSRF;若目标驱动存在 autoDeserialize/INIT=RUNSCRIPT/allowLoadLocalInfile 等特性则可能进一步 RCE);可指定 JndiDataAdapter.dataSourceName(随后触发 JNDI lookup);若宿主 classpath 上存在带危险 setter 的 DataAdapter 实现,则可直接 RCE。
  • 前提:能控制该属性指向的资源位置(模板属性/上下文属性),而仓库 URL 白名单默认关闭使远程 http(s):// 加载可行。

三、加固建议(按优先级)

  1. 模板可信性(最高优先级)
    • 绝不把 .jrxml / .jasper 当作普通用户数据接收;
    • 服务端模板只读、版本化、纳入代码评审;如必须接收外部模板,做签名/哈希校验 + 人工审核。
  2. 打开两个默认关闭的安全开关(评估兼容性后)
    • net.sf.jasperreports.report.class.filter.enabled=true + net.sf.jasperreports.report.class.whitelist.*(表达式/脚本沙箱);
    • net.sf.jasperreports.repository.url.filter.enabled=true + net.sf.jasperreports.repository.url.whitelist.*(资源 URL 白名单)。
  3. 反序列化
    • 保持 net.sf.jasperreports.deserialization.class.filter.enabled=true;
    • 设置 net.sf.jasperreports.deserialization.byte.count.limit(如 10 MB);
    • 应用层注册 ObjectInputFilter;拒绝外部 .jasper / .jrprint。
  4. 缩小攻击面
    • 7.0 已把功能拆分为可选 jar:只引入业务需要的 ext(不用 Groovy/JavaScript/OLAP/Castor/Servlets 就不要引入);
    • 移除 ext/castor 等 deprecated 模块。
  5. 资源与网络
    • 报表渲染进程限制出网,禁止访问内网网段与云元数据地址(169.254.169.254 等);
    • 文件系统只读、最小权限,禁用 file: 协议(除非确有需要)。
  6. 运行隔离
    • 把填充/导出放到低权限容器或独立进程,配合只读文件系统与 seccomp;
    • 不要依赖 SecurityManager(JDK 17+ 已废弃、21+ 移除)。
  7. 数据层
    • SQL 使用 $P{}(PreparedStatement 占位),避免 $P!{} 原始拼接;限制 $X{} 的使用范围。
  8. 依赖治理:升级/替换老化依赖,把 dependency-check 纳入 CI。
  9. 监控:对反序列化异常、URL 加载失败/被拒绝、模板编译失败等做告警;错误信息不要回显模板路径或 SQL。

四、复现与验证建议(仅限授权环境)

静态定位命令:

# 反序列化入口
grep -rn "ObjectInputStream\|readObject" --include=*.java core/src | grep -v test
# 运行时字节码加载
grep -rn "defineClass\|loadClassFromBytes" --include=*.java core/src
# 资源加载
grep -rn "new URL(\|openStream()" --include=*.java core/src ext
# 安全开关默认值
grep -n "class.filter.enabled\|url.filter.enabled\|allow.doctype" \
  core/src/main/resources/default.jasperreports.properties

动态验证思路(不提供武器化代码):

  1. 表达式执行:在授权测试环境用最小 .jrxml,把 textFieldExpression 设为 Runtime.getRuntime().exec(...),对比 report.class.filter.enabled 开关前后的行为差异。
  2. .jasper 字节码路径:在隔离环境构造带自定义 CompiledClasses 的 .jasper,验证开启反序列化白名单后仍会 defineClass。
  3. SSRF/LFI:模板固定、参数可控时,把子报表/图片/CSV 数据源指向 http://127.0.0.1:<port>/、http://169.254.169.254/、file:///etc/hostname,观察请求与外带效果。
  4. XXE:对 SVG 处理路径投喂外部实体 payload(见 2.6)。
  5. 依赖:mvn org.owasp:dependency-check-maven:check。

五、附录

5.1 关键文件索引

主题 文件
反序列化过滤器 core/.../engine/util/FilteredObjectInputStream.java、DeserializationClassFilter.java、AbstractClassFilter.java
反序列化白名单 core/src/main/resources/default.jasperreports.properties:408-482
对象加载入口 core/.../engine/util/JRLoader.java:109-235
字节码定义/执行 core/.../engine/util/JRClassLoader.java:292-368、core/.../engine/design/JRAbstractJavaCompiler.java:107-175
编译数据 core/.../engine/design/CompiledClasses.java、JRReportCompileData.java、JasperReport.java:74,140
表达式/脚本沙箱 core/.../compilers/ReportClassFilter.java、ext/groovy/.../JRGroovyCompiler.java:94、ext/javascript/.../ReportClassShutter.java:47
资源 URL 过滤 core/.../repo/DefaultRepositoryService.java:60-146、default.jasperreports.properties:358-359
子报表加载 core/.../engine/fill/JRFillSubreport.java:459-482
HTTP 数据源 ext/data-adapters-http/.../HttpDataService.java:571-612
XML 安全配置 core/.../engine/util/JRXmlUtils.java:220-257、engine/xml/JacksonReportLoader.java:111-114
JNDI ext/data-adapters/.../jndi/JndiDataAdapterService.java:55-95
官方安全配置文档 core/config.reference.xml:5968-6084

5.2 参考链接


六、动态验证(本地实测,2026-09-09)

前面各节为静态审计。本节是实际构建并运行的结果,用于把"可疑"变成"已确认"。原仓库保持只读,源码复制到工作区后构建。

环境:SDKMAN OpenJDK 17.0.19 + Apache Maven 3.9.9;构建 tools/metadata、core、ext/jdt(master-SNAPSHOT,源码 HEAD a93869a1a)→ BUILD SUCCESS(4m33s)。未使用官方发行 jar,验证的是本地源码。

PoC 验证目标 默认配置 开启缓解开关 判定
PoC1 .jrxml 表达式 → 命令执行 ✅ id 输出写入 marker ❌ Class java.lang.Runtime is not visible to reports. 确认,开关有效
PoC2 .jasper 内嵌字节码 → RCE(反序列化白名单默认开启) ✅ id 输出写入 marker 白名单无法拦截 确认,白名单无效
PoC3 仓库层 SSRF / 本地文件读取 ✅ /etc/hosts 读取 368 字节;HTTP 命中 1 次 url.filter.enabled=true 全阻断;白名单按主机放行 确认,开关有效
PoC4 styled-text 字段值 → Java 反序列化 ❌ Class Probe is not visible to value deserialization. 加入 value 白名单后 Probe.readObject() 执行 sink 确认,默认白名单是唯一屏障
PoC5 可信模板 + 数据可控子报表路径 → .jasper → RCE ✅ id 输出写入 marker — 数据可控 RCE 端到端确认
PoC6 报表图片表达式 → SSRF ✅ HTTP 命中 1 次 过滤开启命中 0;白名单放行命中 1 确认

关键证据摘录:

# PoC1 默认:表达式直接执行
[*] filled pages=1
RCE CONFIRMED: uid=501(h) gid=20(staff) ...

# PoC1 report.class.filter.enabled=true:被类加载过滤器拦截
Caused by: JRRuntimeException: Class java.lang.Runtime is not visible to reports.
  at AbstractClassFilter.checkClassVisibility(AbstractClassFilter.java:93)
  at JRClassLoader.loadClass(JRClassLoader.java:386)

# PoC2:.jasper 内嵌字节码,经 JRLoader(反序列化白名单)加载后仍执行
[*] malicious evaluator bytecode: 1367 bytes
[*] reloaded through deserialization whitelist: net.sf.jasperreports.engine.JasperReport
[*] filled pages=1
RCE CONFIRMED (whitelist did NOT stop it): uid=501(h) ...

# PoC5:主模板可信,仅 $P{SUBREPORT} 的值(数据)指向恶意 .jasper
[*] master compiled OK
[*] master filled pages=1
DATA-CONTROLLED RCE CONFIRMED: uid=501(h) ...

复现位置:/jr-lab/(VERIFICATION.md、poc/、run.sh、build.log)。

6.1 默认配置自证(无任何配置覆盖)

上面"默认配置"的判定不是推测,而是运行时读取的实际值。验证时 classpath 不含任何 jasperreports.properties、命令行无 -D(SYSTEM_PROPS = null):

EFFECTIVE net.sf.jasperreports.report.class.filter.enabled          = false
EFFECTIVE net.sf.jasperreports.repository.url.filter.enabled        = false
EFFECTIVE net.sf.jasperreports.default.file.repository.enabled      = true
EFFECTIVE net.sf.jasperreports.deserialization.class.filter.enabled = true
EFFECTIVE net.sf.jasperreports.query.parameter.clause.enabled       = true
EFFECTIVE net.sf.jasperreports.xml.allow.doctype                    = false

负向对照(证明反序列化白名单确实在生效):同一 JVM 中序列化 java.io.File 再 JRLoader.loadObject →

[*] NEGATIVE CONTROL OK: filter rejected java.io.File -> Class java.io.File is not visible to deserialization.

即过滤器确实开启,但 PoC2 的 .jasper 内嵌字节码仍绕过它执行。同一干净 classpath 下重跑:PoC1/PoC2/PoC3/PoC5Remote/PoC6 全部复现(PoC5Remote 为"可信模板 + 数据可控 URL → 远程下载 .jasper → 执行",下载命中 1 次)。

6.2 前提条件(避免误读为"无条件漏洞")

PoC 需要模板可控 需要应用把不可信输入接到该表达式
PoC1 ✅ 攻击者提供 .jrxml —
PoC2 ✅ 攻击者提供 .jasper —
PoC3 ❌ ✅ 位置值来自参数/字段或应用输入
PoC4 ❌ ✅ 字段用 markup="styled" 且值可控;默认白名单会拦截(非默认 RCE)
PoC5Remote ❌ ✅ 可信模板把 $P{}/$F{} 用作子报表位置
PoC6 ❌ ✅ 可信模板把 $P{}/$F{} 用作图片位置

cfg/report-on、cfg/url-block、cfg/url-allow-ssrf、cfg/value-allow 这些配置只用于展示缓解开关,不参与漏洞复现;漏洞复现使用的是空配置目录与干净 classpath。

对结论的修正:2.11 的"数据可控 .jasper → RCE"已由 PoC5 端到端证实;2.12 的 SSRF/LFI 已由 PoC3 + PoC6 证实(仓库层与报表图片层);2.13.2 的 value 反序列化 sink 已由 PoC4 证实(默认被窄白名单拦截)。SQL 注入(数据面)与命令执行(Runtime.exec 数据面)在实测中未出现,与 1.3 的判定一致。


报告生成:静态审计 + 本地动态验证;原仓库未做任何修改。

正文完
 0