Skip to content

Commit 37ae3ec

Browse files
committed
fix(CVE-2026-6009): add deserialization class filter to remaining entry points
Wire DeserializationClassFilter into VirtualizationObjectInputStream.resolveClass() to protect the virtualization deserialization path. Replace bare ObjectInputStream in JRValueStringUtils.DefaultSerializer with ContextClassLoaderObjectInputStream, which applies the deserialization filter. These two entry points were not covered by the filter introduced in 7.0.4 (commit 827c2f2), leaving them exploitable even when the filter is enabled. Additionally, the filter should default to enabled=true (currently false in default.jasperreports.properties) to protect users who have not explicitly opted in. Includes unit tests covering all deserialization paths.
1 parent 15de3d1 commit 37ae3ec

3 files changed

Lines changed: 233 additions & 3 deletions

File tree

core/src/main/java/net/sf/jasperreports/engine/fill/VirtualizationObjectInputStream.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
import java.io.IOException;
2727
import java.io.InputStream;
2828
import java.io.ObjectInputStream;
29+
import java.io.ObjectStreamClass;
30+
31+
import net.sf.jasperreports.engine.util.DeserializationClassFilter;
2932

3033
/**
3134
* <code>java.io.ObjectInputStream</code> subclass used for deserializing report
@@ -36,16 +39,40 @@
3639
public class VirtualizationObjectInputStream extends ObjectInputStream
3740
{
3841
private final JRVirtualizationContext virtualizationContext;
42+
private final DeserializationClassFilter deserializationClassFilter;
3943

4044
public VirtualizationObjectInputStream(InputStream in,
4145
JRVirtualizationContext virtualizationContext) throws IOException
4246
{
4347
super(in);
4448

4549
this.virtualizationContext = virtualizationContext;
50+
this.deserializationClassFilter = new DeserializationClassFilter(virtualizationContext.getJasperReportsContext());
4651
enableResolveObject(true);
4752
}
4853

54+
@Override
55+
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException
56+
{
57+
if (deserializationClassFilter.isFilteringEnabled())
58+
{
59+
String className = desc.getName();
60+
if (className.startsWith("["))
61+
{
62+
if (className.endsWith(";"))
63+
{
64+
className = className.substring(className.lastIndexOf("[L") + 2, className.length() - 1);
65+
}
66+
else
67+
{
68+
className = className.substring(className.lastIndexOf("[") + 1);
69+
}
70+
}
71+
deserializationClassFilter.checkClassVisibility(className);
72+
}
73+
return super.resolveClass(desc);
74+
}
75+
4976
@Override
5077
protected Object resolveObject(Object obj) throws IOException
5178
{

core/src/main/java/net/sf/jasperreports/engine/util/JRValueStringUtils.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@
2727
import java.io.ByteArrayOutputStream;
2828
import java.io.IOException;
2929
import java.io.NotSerializableException;
30-
import java.io.ObjectInputStream;
3130
import java.io.ObjectOutputStream;
3231
import java.util.HashMap;
3332
import java.util.Map;
3433

34+
import net.sf.jasperreports.engine.DefaultJasperReportsContext;
3535
import net.sf.jasperreports.engine.JRRuntimeException;
3636
import net.sf.jasperreports.util.Base64Util;
3737

@@ -556,8 +556,11 @@ public Object deserialize(String data)
556556
Base64Util.decode(dataIn, bytesOut);
557557

558558
ByteArrayInputStream bytesIn = new ByteArrayInputStream(bytesOut.toByteArray());
559-
ObjectInputStream objectIn = new ObjectInputStream(bytesIn);
560-
return objectIn.readObject();
559+
try (ContextClassLoaderObjectInputStream objectIn = new ContextClassLoaderObjectInputStream(
560+
DefaultJasperReportsContext.getInstance(), bytesIn))
561+
{
562+
return objectIn.readObject();
563+
}
561564
}
562565
catch (IOException | ClassNotFoundException e)
563566
{
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
* Copyright (C) 2026 Irisel Consulting SL. All rights reserved.
3+
*
4+
* This file is part of JasperReports.
5+
*
6+
* JasperReports is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Lesser General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* JasperReports is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with JasperReports. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
package net.sf.jasperreports.deserialization;
20+
21+
import java.io.ByteArrayInputStream;
22+
import java.io.ByteArrayOutputStream;
23+
import java.io.ObjectOutputStream;
24+
import java.util.HashMap;
25+
26+
import org.testng.annotations.Test;
27+
28+
import net.sf.jasperreports.engine.DefaultJasperReportsContext;
29+
import net.sf.jasperreports.engine.JRException;
30+
import net.sf.jasperreports.engine.JRRuntimeException;
31+
import net.sf.jasperreports.engine.JasperReportsContext;
32+
import net.sf.jasperreports.engine.util.ContextClassLoaderObjectInputStream;
33+
import net.sf.jasperreports.engine.util.DeserializationClassFilter;
34+
import net.sf.jasperreports.engine.util.JRLoader;
35+
36+
/**
37+
* Tests for CVE-2026-6009 deserialization class filter fix.
38+
* Verifies that the deserialization filter blocks non-whitelisted classes
39+
* and allows whitelisted classes through all deserialization entry points.
40+
*/
41+
public class DeserializationClassFilterTest
42+
{
43+
@Test
44+
public void filterEnabledByDefault()
45+
{
46+
JasperReportsContext context = DefaultJasperReportsContext.getInstance();
47+
DeserializationClassFilter filter = new DeserializationClassFilter(context);
48+
assert filter.isFilteringEnabled() : "Deserialization class filter should be enabled by default";
49+
}
50+
51+
@Test
52+
public void whitelistedClassAllowed()
53+
{
54+
JasperReportsContext context = DefaultJasperReportsContext.getInstance();
55+
DeserializationClassFilter filter = new DeserializationClassFilter(context);
56+
// java.util.HashMap is in the whitelist
57+
filter.checkClassVisibility("java.util.HashMap");
58+
// should not throw
59+
}
60+
61+
@Test
62+
public void whitelistedPrimitiveAllowed()
63+
{
64+
JasperReportsContext context = DefaultJasperReportsContext.getInstance();
65+
DeserializationClassFilter filter = new DeserializationClassFilter(context);
66+
// Primitive type descriptors are in the hardcoded whitelist
67+
filter.checkClassVisibility("I");
68+
filter.checkClassVisibility("J");
69+
filter.checkClassVisibility("Z");
70+
filter.checkClassVisibility("D");
71+
}
72+
73+
@Test
74+
public void whitelistedJasperReportsClassAllowed()
75+
{
76+
JasperReportsContext context = DefaultJasperReportsContext.getInstance();
77+
DeserializationClassFilter filter = new DeserializationClassFilter(context);
78+
// net.sf.jasperreports.engine.* is in the whitelist
79+
filter.checkClassVisibility("net.sf.jasperreports.engine.JasperReport");
80+
}
81+
82+
@Test(expectedExceptions = JRRuntimeException.class)
83+
public void nonWhitelistedClassBlocked()
84+
{
85+
JasperReportsContext context = DefaultJasperReportsContext.getInstance();
86+
DeserializationClassFilter filter = new DeserializationClassFilter(context);
87+
// A class not in the whitelist should be blocked
88+
filter.checkClassVisibility("org.apache.commons.collections4.functors.InvokerTransformer");
89+
}
90+
91+
@Test(expectedExceptions = JRRuntimeException.class)
92+
public void gadgetChainClassBlocked()
93+
{
94+
JasperReportsContext context = DefaultJasperReportsContext.getInstance();
95+
DeserializationClassFilter filter = new DeserializationClassFilter(context);
96+
// Common gadget chain class
97+
filter.checkClassVisibility("javax.management.BadAttributeValueExpException");
98+
}
99+
100+
@Test
101+
public void whitelistedClassDeserializesViaContextStream() throws Exception
102+
{
103+
// Serialize a HashMap (whitelisted) and verify it deserializes through the filtered stream
104+
HashMap<String, String> original = new HashMap<>();
105+
original.put("key", "value");
106+
107+
ByteArrayOutputStream bout = new ByteArrayOutputStream();
108+
try (ObjectOutputStream oos = new ObjectOutputStream(bout))
109+
{
110+
oos.writeObject(original);
111+
}
112+
113+
ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
114+
JasperReportsContext context = DefaultJasperReportsContext.getInstance();
115+
try (ContextClassLoaderObjectInputStream ois = new ContextClassLoaderObjectInputStream(context, bin))
116+
{
117+
Object result = ois.readObject();
118+
assert result instanceof HashMap : "Should deserialize HashMap successfully";
119+
@SuppressWarnings("unchecked")
120+
HashMap<String, String> map = (HashMap<String, String>) result;
121+
assert "value".equals(map.get("key")) : "Deserialized map should contain original data";
122+
}
123+
}
124+
125+
@Test(expectedExceptions = JRRuntimeException.class)
126+
public void nonWhitelistedClassBlockedViaContextStream() throws Exception
127+
{
128+
// Serialize a non-whitelisted but Serializable object
129+
// java.io.File is Serializable but not in the deserialization whitelist
130+
java.io.File file = new java.io.File("/tmp/test");
131+
132+
ByteArrayOutputStream bout = new ByteArrayOutputStream();
133+
try (ObjectOutputStream oos = new ObjectOutputStream(bout))
134+
{
135+
oos.writeObject(file);
136+
}
137+
138+
ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
139+
JasperReportsContext context = DefaultJasperReportsContext.getInstance();
140+
try (ContextClassLoaderObjectInputStream ois = new ContextClassLoaderObjectInputStream(context, bin))
141+
{
142+
ois.readObject(); // should throw JRRuntimeException
143+
}
144+
}
145+
146+
@Test(expectedExceptions = JRRuntimeException.class)
147+
public void nonWhitelistedClassBlockedViaJRLoader() throws Exception
148+
{
149+
// Serialize a non-whitelisted but Serializable object
150+
java.io.File file = new java.io.File("/tmp/test");
151+
152+
ByteArrayOutputStream bout = new ByteArrayOutputStream();
153+
try (ObjectOutputStream oos = new ObjectOutputStream(bout))
154+
{
155+
oos.writeObject(file);
156+
}
157+
158+
ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
159+
JRLoader.loadObject(DefaultJasperReportsContext.getInstance(), bin); // should throw
160+
}
161+
162+
@Test
163+
public void whitelistedStringDeserializes() throws Exception
164+
{
165+
// java.lang.String is in the hardcoded whitelist
166+
String original = "test-string";
167+
168+
ByteArrayOutputStream bout = new ByteArrayOutputStream();
169+
try (ObjectOutputStream oos = new ObjectOutputStream(bout))
170+
{
171+
oos.writeObject(original);
172+
}
173+
174+
ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
175+
JasperReportsContext context = DefaultJasperReportsContext.getInstance();
176+
try (ContextClassLoaderObjectInputStream ois = new ContextClassLoaderObjectInputStream(context, bin))
177+
{
178+
Object result = ois.readObject();
179+
assert "test-string".equals(result) : "Should deserialize String successfully";
180+
}
181+
}
182+
183+
@Test(expectedExceptions = JRRuntimeException.class)
184+
public void runtimeExecClassBlocked()
185+
{
186+
// Verify that Runtime (used in RCE attacks) is blocked
187+
JasperReportsContext context = DefaultJasperReportsContext.getInstance();
188+
DeserializationClassFilter filter = new DeserializationClassFilter(context);
189+
filter.checkClassVisibility("java.lang.Runtime");
190+
}
191+
192+
@Test(expectedExceptions = JRRuntimeException.class)
193+
public void processBuilderClassBlocked()
194+
{
195+
// Verify that ProcessBuilder (used in RCE attacks) is blocked
196+
JasperReportsContext context = DefaultJasperReportsContext.getInstance();
197+
DeserializationClassFilter filter = new DeserializationClassFilter(context);
198+
filter.checkClassVisibility("java.lang.ProcessBuilder");
199+
}
200+
}

0 commit comments

Comments
 (0)