diff --git a/jasperreports/pom.xml b/jasperreports/pom.xml index 264bb6965b..fd1c58fdaa 100644 --- a/jasperreports/pom.xml +++ b/jasperreports/pom.xml @@ -3,7 +3,7 @@ 4.0.0 net.sf.jasperreports jasperreports - 6.4.0.3-Rx + 6.4.0.4-Rx jar JasperReports http://jasperreports.sourceforge.net @@ -179,13 +179,9 @@ RXLOGIX-thirdparty RXLogix Thirdparty Repository - http://10.100.21.16:8080/repository/thirdparty + https://nexus-repo-eng.rxlogix.com/repository/thirdparty/ + - - jr-ce-snapshots - JasperReports CE Snapshots - http://jaspersoft.jfrog.io/jaspersoft/jr-ce-snapshots - 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(); + } +}