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 |
供应链风险 |
总体结论:
- 代码本身未发现"仅靠不可信数据"即可打通的未授权 RCE;两个公开反序列化 CVE 在本版本均已修复。
- 但 JasperReports 的安全性完全取决于模板是否可信。只要能控制
.jrxml或.jasper,攻击者即可获得任意代码执行——这不是 bug,而是"模板即代码"的设计结果。 - 更值得注意的是: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 链"路径,但无法阻止"内嵌字节码"路径。
原理:
.jasper是 Java 序列化对象(JasperReport),加载入口JRLoader.loadObject()直接readObject():core/.../engine/util/JRLoader.java:138 / 182 / 228- 通过
ContextClassLoaderObjectInputStream(core/.../engine/util/ContextClassLoaderObjectInputStream.java:55)套用类白名单过滤器。
- 序列化对象中保存了编译后的表达式字节码:
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[]>)。
- 填充报表时,这些字节码被
defineClass并实例化:JRAbstractCompiler.createEvaluator()→loadEvaluator()→JRAbstractJavaCompiler.loadEvaluator()→JRClassLoader.loadClassFromBytes()→defineClass+newInstance。
- 为什么白名单放行:默认反序列化白名单包含
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 已修复,但防护属于"缓解"而非"根治":
- 白名单较宽(
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 面"。 - 类名解析走 TCCL:
ContextClassLoaderObjectInputStream.resolveClass()(第 73–100 行)在父加载器失败时用线程上下文类加载器Class.forName(name, false, contextClassLoader)——便于应用加载自身类,也意味着"白名单 + 类路径"共同决定可见类。 - 无字节数上限:
net.sf.jasperreports.deserialization.byte.count.limit默认0(不限制,FilteredObjectInputStream.java:51-52,77)→ 超大序列化流可造成内存/CPU DoS。 - 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)://加载可行。
三、加固建议(按优先级)
- 模板可信性(最高优先级)
- 绝不把
.jrxml/.jasper当作普通用户数据接收; - 服务端模板只读、版本化、纳入代码评审;如必须接收外部模板,做签名/哈希校验 + 人工审核。
- 绝不把
- 打开两个默认关闭的安全开关(评估兼容性后)
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 白名单)。
- 反序列化
- 保持
net.sf.jasperreports.deserialization.class.filter.enabled=true; - 设置
net.sf.jasperreports.deserialization.byte.count.limit(如 10 MB); - 应用层注册
ObjectInputFilter;拒绝外部.jasper/.jrprint。
- 保持
- 缩小攻击面
- 7.0 已把功能拆分为可选 jar:只引入业务需要的 ext(不用 Groovy/JavaScript/OLAP/Castor/Servlets 就不要引入);
- 移除
ext/castor等 deprecated 模块。
- 资源与网络
- 报表渲染进程限制出网,禁止访问内网网段与云元数据地址(169.254.169.254 等);
- 文件系统只读、最小权限,禁用
file:协议(除非确有需要)。
- 运行隔离
- 把填充/导出放到低权限容器或独立进程,配合只读文件系统与 seccomp;
- 不要依赖
SecurityManager(JDK 17+ 已废弃、21+ 移除)。
- 数据层
- SQL 使用
$P{}(PreparedStatement 占位),避免$P!{}原始拼接;限制$X{}的使用范围。
- SQL 使用
- 依赖治理:升级/替换老化依赖,把 dependency-check 纳入 CI。
- 监控:对反序列化异常、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
动态验证思路(不提供武器化代码):
- 表达式执行:在授权测试环境用最小
.jrxml,把textFieldExpression设为Runtime.getRuntime().exec(...),对比report.class.filter.enabled开关前后的行为差异。 .jasper字节码路径:在隔离环境构造带自定义CompiledClasses的.jasper,验证开启反序列化白名单后仍会defineClass。- SSRF/LFI:模板固定、参数可控时,把子报表/图片/CSV 数据源指向
http://127.0.0.1:<port>/、http://169.254.169.254/、file:///etc/hostname,观察请求与外带效果。 - XXE:对 SVG 处理路径投喂外部实体 payload(见 2.6)。
- 依赖:
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 参考链接
- CVE-2025-10492 — NVD
- GHSA-7c3f-cg9x-f3gr — GitHub Advisory
- CVE-2026-6009 — NVD
- GHSA-9wxq-mwqw-8hhg — GitHub Advisory
- CVE-2025-10492 — GitLab Advisory DB(修复版本 7.0.4)
- Jaspersoft 安全公告(CVE-2025-10492)
- Jaspersoft 安全公告(CVE-2026-6009)
- JasperReports 仓库
六、动态验证(本地实测,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 的判定一致。
报告生成:静态审计 + 本地动态验证;原仓库未做任何修改。