diff --git a/jasperreports/src/jasperreports_messages.properties b/jasperreports/src/jasperreports_messages.properties
index c13a3769ff..8ed96192fd 100644
--- a/jasperreports/src/jasperreports_messages.properties
+++ b/jasperreports/src/jasperreports_messages.properties
@@ -565,3 +565,5 @@ net.sf.jasperreports.exception.phantomjs.request.timed.out=The request to Phanto
# deserialization class filter (CVE-2025-10492)
net.sf.jasperreports.exception.deserialization.byte.count.limit.exceeded=Deserialization byte count limit of {0} has been exceeded.
net.sf.jasperreports.exception.deserialization.class.not.visible=Class {0} is not visible to deserialization.
+# value deserialization class filter (CVE-2026-6009)
+net.sf.jasperreports.exception.value.deserialization.class.not.visible=Class {0} is not visible to value deserialization.
diff --git a/jasperreports/src/net/sf/jasperreports/engine/util/AbstractClassFilter.java b/jasperreports/src/net/sf/jasperreports/engine/util/AbstractClassFilter.java
index a6d7f1dbd6..b4e57596ba 100644
--- a/jasperreports/src/net/sf/jasperreports/engine/util/AbstractClassFilter.java
+++ b/jasperreports/src/net/sf/jasperreports/engine/util/AbstractClassFilter.java
@@ -38,7 +38,7 @@
/**
* @author Lucian Chirita (lucianc@users.sourceforge.net)
*/
-public abstract class AbstractClassFilter implements ClassLoaderFilter
+public abstract class AbstractClassFilter implements DeserializationFilter
{
protected abstract String getClassFilterEnabledPropertyName();
diff --git a/jasperreports/src/net/sf/jasperreports/engine/util/DeserializationFilter.java b/jasperreports/src/net/sf/jasperreports/engine/util/DeserializationFilter.java
new file mode 100644
index 0000000000..424f08e7bc
--- /dev/null
+++ b/jasperreports/src/net/sf/jasperreports/engine/util/DeserializationFilter.java
@@ -0,0 +1,34 @@
+/*
+ * JasperReports - Free Java Reporting Library.
+ * Copyright (C) 2001 - 2025 Cloud Software Group, Inc. All rights reserved.
+ * http://www.jaspersoft.com
+ *
+ * Unless you have purchased a commercial license agreement from Jaspersoft,
+ * the following license terms apply:
+ *
+ * This program is part of JasperReports.
+ *
+ * JasperReports is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * JasperReports is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with JasperReports. If not, see .
+ */
+package net.sf.jasperreports.engine.util;
+
+/**
+ * @author Lucian Chirita (lucianc@users.sourceforge.net)
+ */
+public interface DeserializationFilter extends ClassLoaderFilter
+{
+
+ boolean isFilteringEnabled();
+
+}
diff --git a/jasperreports/src/net/sf/jasperreports/engine/util/FilteredObjectInputStream.java b/jasperreports/src/net/sf/jasperreports/engine/util/FilteredObjectInputStream.java
new file mode 100644
index 0000000000..e4b600aafa
--- /dev/null
+++ b/jasperreports/src/net/sf/jasperreports/engine/util/FilteredObjectInputStream.java
@@ -0,0 +1,101 @@
+/*
+ * JasperReports - Free Java Reporting Library.
+ * Copyright (C) 2001 - 2025 Cloud Software Group, Inc. All rights reserved.
+ * http://www.jaspersoft.com
+ *
+ * Unless you have purchased a commercial license agreement from Jaspersoft,
+ * the following license terms apply:
+ *
+ * This program is part of JasperReports.
+ *
+ * JasperReports is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * JasperReports is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with JasperReports. If not, see .
+ */
+package net.sf.jasperreports.engine.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectStreamClass;
+
+import net.sf.jasperreports.engine.JRPropertiesUtil;
+import net.sf.jasperreports.engine.JasperReportsContext;
+
+/**
+ * A subclass of {@link ObjectInputStream} that filters the classes encountered
+ * in the stream against a configurable {@link DeserializationFilter}, and that
+ * optionally enforces the deserialization byte count limit.
+ *
+ * @author Teodor Danciu (teodord@users.sourceforge.net)
+ */
+public class FilteredObjectInputStream extends ObjectInputStream
+{
+ protected final JasperReportsContext jasperReportsContext;
+
+ private DeserializationFilter deserializationFilter;
+
+ /**
+ * Creates an object input stream that reads data from the specified
+ * {@link InputStream}.
+ *
+ * @param in the input stream to read data from
+ * @throws IOException
+ * @see ObjectInputStream#ObjectInputStream(InputStream)
+ */
+ public FilteredObjectInputStream(JasperReportsContext jasperReportsContext, InputStream in,
+ DeserializationFilter deserializationFilter) throws IOException
+ {
+ super(wrapInputStream(jasperReportsContext, in));
+
+ this.jasperReportsContext = jasperReportsContext;
+ this.deserializationFilter = deserializationFilter;
+ }
+
+ private static InputStream wrapInputStream(JasperReportsContext jasperReportsContext, InputStream is)
+ {
+ String byteCountLimitProp = JRPropertiesUtil.getInstance(jasperReportsContext).getProperty(
+ ContextClassLoaderObjectInputStream.PROPERTY_BYTE_COUNT_LIMIT);
+ long byteCountLimit = (byteCountLimitProp == null || byteCountLimitProp.trim().length() == 0)
+ ? 0L : Long.parseLong(byteCountLimitProp.trim());
+ return byteCountLimit == 0 ? is : new CountInputStream(is, byteCountLimit);
+ }
+
+ public JasperReportsContext getJasperReportsContext()
+ {
+ return jasperReportsContext;
+ }
+
+ @Override
+ protected Class> resolveClass(ObjectStreamClass desc) throws IOException,
+ ClassNotFoundException
+ {
+ if (deserializationFilter.isFilteringEnabled())
+ {
+ String className = desc.getName();
+ if (className.startsWith("["))
+ {
+ if (className.endsWith(";"))
+ {
+ className = className.substring(className.lastIndexOf("[L") + 2, className.length() - 1);
+ }
+ else
+ {
+ className = className.substring(className.lastIndexOf("[") + 1);
+ }
+ }
+ deserializationFilter.checkClassVisibility(className);
+ }
+
+ return super.resolveClass(desc);
+ }
+}
diff --git a/jasperreports/src/net/sf/jasperreports/engine/util/JRValueStringUtils.java b/jasperreports/src/net/sf/jasperreports/engine/util/JRValueStringUtils.java
index 0fadf7d75f..ea3430051d 100644
--- a/jasperreports/src/net/sf/jasperreports/engine/util/JRValueStringUtils.java
+++ b/jasperreports/src/net/sf/jasperreports/engine/util/JRValueStringUtils.java
@@ -32,7 +32,9 @@
import java.util.HashMap;
import java.util.Map;
+import net.sf.jasperreports.engine.DefaultJasperReportsContext;
import net.sf.jasperreports.engine.JRRuntimeException;
+import net.sf.jasperreports.engine.JasperReportsContext;
import org.w3c.tools.codec.Base64Decoder;
import org.w3c.tools.codec.Base64Encoder;
@@ -560,7 +562,9 @@ public Object deserialize(String data)
dec.process();
ByteArrayInputStream bytesIn = new ByteArrayInputStream(bytesOut.toByteArray());
- ObjectInputStream objectIn = new ObjectInputStream(bytesIn);
+ JasperReportsContext context = DefaultJasperReportsContext.getInstance();
+ ObjectInputStream objectIn = new FilteredObjectInputStream(
+ context, bytesIn, new ValueClassFilter(context));
return objectIn.readObject();
}
catch (IOException e)
diff --git a/jasperreports/src/net/sf/jasperreports/engine/util/ValueClassFilter.java b/jasperreports/src/net/sf/jasperreports/engine/util/ValueClassFilter.java
new file mode 100644
index 0000000000..308202285f
--- /dev/null
+++ b/jasperreports/src/net/sf/jasperreports/engine/util/ValueClassFilter.java
@@ -0,0 +1,66 @@
+/*
+ * JasperReports - Free Java Reporting Library.
+ * Copyright (C) 2001 - 2025 Cloud Software Group, Inc. All rights reserved.
+ * http://www.jaspersoft.com
+ *
+ * Unless you have purchased a commercial license agreement from Jaspersoft,
+ * the following license terms apply:
+ *
+ * This program is part of JasperReports.
+ *
+ * JasperReports is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * JasperReports is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with JasperReports. If not, see .
+ */
+package net.sf.jasperreports.engine.util;
+
+import net.sf.jasperreports.engine.JRPropertiesUtil;
+import net.sf.jasperreports.engine.JasperReportsContext;
+
+/**
+ * @author Lucian Chirita (lucianc@users.sourceforge.net)
+ */
+public class ValueClassFilter extends AbstractClassFilter
+{
+ public static final String PROPERTY_PREFIX_CLASS_WHITELIST =
+ JRPropertiesUtil.PROPERTY_PREFIX + "value.deserialization.class.whitelist.";
+
+ public static final String EXCEPTION_MESSAGE_KEY_CLASS_NOT_VISIBLE = "value.deserialization.class.not.visible";
+
+ public ValueClassFilter(JasperReportsContext jasperReportsContext)
+ {
+ super(jasperReportsContext);
+ }
+
+ @Override
+ protected String getClassFilterEnabledPropertyName()
+ {
+ return DeserializationClassFilter.PROPERTY_CLASS_FILTER_ENABLED;
+ }
+
+ @Override
+ protected String getClassWhitelistPropertyPrefix()
+ {
+ return PROPERTY_PREFIX_CLASS_WHITELIST;
+ }
+
+ @Override
+ protected String getClassNotVisibleExceptionMessageKey()
+ {
+ return EXCEPTION_MESSAGE_KEY_CLASS_NOT_VISIBLE;
+ }
+
+ @Override
+ protected void addHardcodedWhitelist(StandardClassWhitelist whitelist)
+ {
+ }
+}
diff --git a/jasperreports/tests/net/sf/jasperreports/engine/util/JRValueStringUtilsFilterTest.java b/jasperreports/tests/net/sf/jasperreports/engine/util/JRValueStringUtilsFilterTest.java
new file mode 100644
index 0000000000..3ce83cdd89
--- /dev/null
+++ b/jasperreports/tests/net/sf/jasperreports/engine/util/JRValueStringUtilsFilterTest.java
@@ -0,0 +1,219 @@
+/*
+ * JasperReports - Free Java Reporting Library.
+ * Copyright (C) 2001 - 2016 TIBCO Software Inc. All rights reserved.
+ * http://www.jaspersoft.com
+ *
+ * Unless you have purchased a commercial license agreement from Jaspersoft,
+ * the following license terms apply:
+ *
+ * This program is part of JasperReports.
+ *
+ * JasperReports is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * JasperReports is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with JasperReports. If not, see .
+ */
+package net.sf.jasperreports.engine.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+
+import net.sf.jasperreports.engine.JRRuntimeException;
+import net.sf.jasperreports.engine.SimpleJasperReportsContext;
+
+import org.testng.annotations.Test;
+
+/**
+ * Reproducer for CVE-2026-6009 — uncontrolled deserialization of arbitrary
+ * classes through report value strings.
+ *
+ * {@link JRValueStringUtils} deserializes BASE64-encoded value strings of
+ * non-built-in types through a plain ObjectInputStream. Without
+ * the value-deserialization class filter, an attacker who controls a value
+ * string (e.g. a default value in a .jasper file or a subreport
+ * value) can have any Serializable class on the classpath instantiated and its
+ * custom readObject executed, leading to remote code execution.
+ *
+ * The fix routes value deserialization through {@link FilteredObjectInputStream}
+ * using a {@link ValueClassFilter}, which is gated by the existing
+ * net.sf.jasperreports.deserialization.class.filter.enabled flag
+ * (default true) and a dedicated
+ * net.sf.jasperreports.value.deserialization.class.whitelist.*
+ * namespace that is empty by default.
+ */
+public class JRValueStringUtilsFilterTest
+{
+ private static final String FILTER_ENABLED_PROPERTY =
+ "net.sf.jasperreports.deserialization.class.filter.enabled";
+ private static final String VALUE_WHITELIST_PROPERTY_PREFIX =
+ "net.sf.jasperreports.value.deserialization.class.whitelist.";
+ private static final String EXCEPTION_KEY_NOT_VISIBLE =
+ "value.deserialization.class.not.visible";
+
+ /**
+ * Stand-in for an RCE gadget. Demonstrates that arbitrary Serializable
+ * classes are resolved and have their readObject invoked
+ * through value deserialization when no class filter is in place.
+ */
+ public static class EvilGadget implements Serializable
+ {
+ private static final long serialVersionUID = 1L;
+ public static volatile boolean executed;
+
+ private void readObject(java.io.ObjectInputStream in)
+ throws IOException, ClassNotFoundException
+ {
+ in.defaultReadObject();
+ executed = true;
+ }
+ }
+
+ @Test
+ public void filterDisabled_gadgetExecutes() throws IOException, ClassNotFoundException
+ {
+ EvilGadget.executed = false;
+ SimpleJasperReportsContext ctx = new SimpleJasperReportsContext();
+ ctx.setProperty(FILTER_ENABLED_PROPERTY, "false");
+
+ byte[] bytes = serialize(new EvilGadget());
+ FilteredObjectInputStream in = new FilteredObjectInputStream(
+ ctx, new ByteArrayInputStream(bytes), new ValueClassFilter(ctx));
+ try
+ {
+ Object read = in.readObject();
+ assert read instanceof EvilGadget;
+ }
+ finally
+ {
+ in.close();
+ }
+ assert EvilGadget.executed : "EvilGadget.readObject should run when filter is disabled";
+ }
+
+ @Test
+ public void filterEnabled_gadgetBlocked() throws IOException
+ {
+ EvilGadget.executed = false;
+ SimpleJasperReportsContext ctx = new SimpleJasperReportsContext();
+ ctx.setProperty(FILTER_ENABLED_PROPERTY, "true");
+
+ byte[] bytes = serialize(new EvilGadget());
+ FilteredObjectInputStream in = new FilteredObjectInputStream(
+ ctx, new ByteArrayInputStream(bytes), new ValueClassFilter(ctx));
+ try
+ {
+ in.readObject();
+ assert false : "Expected JRRuntimeException for non-whitelisted class";
+ }
+ catch (JRRuntimeException e)
+ {
+ assert EXCEPTION_KEY_NOT_VISIBLE.equals(e.getMessageKey()) :
+ "Unexpected message key: " + e.getMessageKey();
+ }
+ catch (ClassNotFoundException e)
+ {
+ throw new AssertionError("Filter should reject the class before ClassNotFoundException is raised");
+ }
+ finally
+ {
+ in.close();
+ }
+ assert !EvilGadget.executed : "EvilGadget.readObject must not run when filter blocks the class";
+ }
+
+ @Test
+ public void filterEnabled_inheritsSharedDeserializationWhitelist() throws IOException, ClassNotFoundException
+ {
+ // AbstractClassFilter loads the shared DeserializationClassWhitelist extensions for every
+ // filter (upstream behaviour), so a class already on the curated report whitelist
+ // (e.g. java.lang.String) is permitted through value deserialization too. The fix tightens
+ // the previously-unfiltered value path down to that curated whitelist; arbitrary attacker
+ // classes such as EvilGadget remain blocked (see filterEnabled_gadgetBlocked).
+ SimpleJasperReportsContext ctx = new SimpleJasperReportsContext();
+ ctx.setProperty(FILTER_ENABLED_PROPERTY, "true");
+
+ String original = "a plain string";
+ byte[] bytes = serialize(original);
+ FilteredObjectInputStream in = new FilteredObjectInputStream(
+ ctx, new ByteArrayInputStream(bytes), new ValueClassFilter(ctx));
+ try
+ {
+ Object read = in.readObject();
+ assert original.equals(read) : "Report-whitelisted java.lang.String should round-trip";
+ }
+ finally
+ {
+ in.close();
+ }
+ }
+
+ @Test
+ public void filterEnabled_valueWhitelistNamespaceHonored() throws IOException, ClassNotFoundException
+ {
+ // EvilGadget is not on any report whitelist; whitelisting it through the value-specific
+ // namespace must let it pass, proving the value.* prefix is wired into ValueClassFilter.
+ EvilGadget.executed = false;
+ SimpleJasperReportsContext ctx = new SimpleJasperReportsContext();
+ ctx.setProperty(FILTER_ENABLED_PROPERTY, "true");
+ ctx.setProperty(VALUE_WHITELIST_PROPERTY_PREFIX + "test", EvilGadget.class.getName());
+
+ byte[] bytes = serialize(new EvilGadget());
+ FilteredObjectInputStream in = new FilteredObjectInputStream(
+ ctx, new ByteArrayInputStream(bytes), new ValueClassFilter(ctx));
+ try
+ {
+ Object read = in.readObject();
+ assert read instanceof EvilGadget : "Value-whitelisted class should round-trip";
+ }
+ finally
+ {
+ in.close();
+ }
+ assert EvilGadget.executed : "Value-whitelisted class should deserialize normally";
+ }
+
+ @Test
+ public void valueStringRoundTrip_blocksGadgetByDefault()
+ {
+ // end-to-end through the public API, using the default context (filter enabled by default)
+ EvilGadget.executed = false;
+ String data = JRValueStringUtils.serialize(EvilGadget.class.getName(), new EvilGadget());
+ try
+ {
+ JRValueStringUtils.deserialize(EvilGadget.class.getName(), data);
+ assert false : "Expected JRRuntimeException for non-whitelisted value class";
+ }
+ catch (JRRuntimeException e)
+ {
+ assert EXCEPTION_KEY_NOT_VISIBLE.equals(e.getMessageKey()) :
+ "Unexpected message key: " + e.getMessageKey();
+ }
+ assert !EvilGadget.executed : "EvilGadget.readObject must not run through JRValueStringUtils";
+ }
+
+ private static byte[] serialize(Object obj) throws IOException
+ {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ObjectOutputStream out = new ObjectOutputStream(baos);
+ try
+ {
+ out.writeObject(obj);
+ }
+ finally
+ {
+ out.close();
+ }
+ return baos.toByteArray();
+ }
+}