This is an automated email from the ASF dual-hosted git repository. henrib pushed a commit to branch JEXL-468b in repository https://gitbox.apache.org/repos/asf/commons-jexl.git
commit 86ea6a3e36a487c69fe01f869e761340e7a6f917 Author: Henrib <[email protected]> AuthorDate: Tue Aug 25 17:13:22 2026 +0200 [JEXL-468] Preserve deny-all package semantics when compose() adds class-specific exceptions When a package was previously marked as NOJEXL_PACKAGE (whole-package denial, e.g. "com.example.internal {}") and compose() is called with rules that add class-specific exceptions to that same package, the NOJEXL_PACKAGE sentinel was replaced by a plain NoJexlPackage whose getNoJexl() returns null for unlisted classes — causing deny(Class) to fall back to JEXL_CLASS (allow) for every class not explicitly mentioned. Add DenyAllPackage as the symmetric counterpart of JexlPackage: it returns NOJEXL_CLASS for any class not explicitly listed, so the deny-all-unlisted semantics of the base are preserved while individually declared exceptions ("+SafeClass {}") are honoured. PermissionsParser.readPackages() creates a DenyAllPackage (instead of NoJexlPackage) when merging rules into a package that was previously NOJEXL_PACKAGE or DenyAllPackage and no explicit polarity sign was given in the compose source. Co-Authored-By: Claude <[email protected]> --- .../jexl3/internal/introspection/Permissions.java | 22 +++++++++++++++++++ .../internal/introspection/PermissionsParser.java | 15 ++++++++++--- .../commons/jexl3/ComposePermissionsTest.java | 25 ++++++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java index 5180b740..73e1dd65 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java @@ -251,6 +251,28 @@ public class Permissions implements JexlPermissions { } } + /** + * A package where ALL unlisted classes are denied. + * <p>The symmetric counterpart of {@link JexlPackage}: created by {@code compose()} when class-specific + * exceptions are added to a package previously marked as {@link Markers#NOJEXL_PACKAGE}. + * The deny-all-unlisted semantics of the base are preserved while allowing individually declared classes.</p> + */ + static class DenyAllPackage extends NoJexlPackage { + DenyAllPackage(final Map<String, NoJexlClass> map) { + super(map); + } + + @Override + NoJexlClass getNoJexl(final Class<?> clazz) { + final NoJexlClass njc = nojexl.get(classKey(clazz)); + return njc != null ? njc : NOJEXL_CLASS; + } + + @Override public NoJexlPackage copy() { + return new DenyAllPackage(copyMap(nojexl)); + } + } + /** * Holder for the singleton allow/deny markers and the {@code UNRESTRICTED} permission. * <p>These live in their own class rather than directly in {@link Permissions} to break a diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/PermissionsParser.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/PermissionsParser.java index 4ffd4efc..cdb3baff 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/PermissionsParser.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/PermissionsParser.java @@ -404,9 +404,18 @@ public class PermissionsParser { // keep the existing specification if any, otherwise default to deny final boolean deny = specified == null ? !(p instanceof Permissions.JexlPackage) : specified; final Map<String, Permissions.NoJexlClass> pkgMap = p == null ? null : p.nojexl; - return deny - ? new Permissions.NoJexlPackage(pkgMap) - : new Permissions.JexlPackage(pkgMap); + if (deny) { + // when no explicit sign was given and the existing entry denies all + // unlisted classes (NOJEXL_PACKAGE or DenyAllPackage), preserve that + // deny-all-unlisted semantics for classes not covered by the new rules + final boolean denyAll = specified == null + && (p == Permissions.Markers.NOJEXL_PACKAGE + || p instanceof Permissions.DenyAllPackage); + return denyAll + ? new Permissions.DenyAllPackage(pkgMap) + : new Permissions.NoJexlPackage(pkgMap); + } + return new Permissions.JexlPackage(pkgMap); } ); i += 1; diff --git a/src/test/java/org/apache/commons/jexl3/ComposePermissionsTest.java b/src/test/java/org/apache/commons/jexl3/ComposePermissionsTest.java index 10143788..8730c42c 100644 --- a/src/test/java/org/apache/commons/jexl3/ComposePermissionsTest.java +++ b/src/test/java/org/apache/commons/jexl3/ComposePermissionsTest.java @@ -115,6 +115,31 @@ class ComposePermissionsTest extends JexlTestCase { assertTrue(composed.allow(java.io.StringWriter.class)); } + @Test + void testComposePreservesPackageDenialWithException() throws Exception { + // When compose() adds a class-specific exception to a package previously marked NOJEXL_PACKAGE, + // ALL other classes in that package must remain denied (deny-all-unlisted semantics preserved). + final JexlPermissions base = JexlPermissions.parse("java.lang.*", "java.net {}"); + // before: all of java.net is denied + assertFalse(base.allow(java.net.URI.class)); + assertFalse(base.allow(java.net.URL.class)); + + // compose adds an exception for URI; URL (unlisted) must remain denied + final JexlPermissions withException = base.compose("java.net { +URI {} }"); + assertTrue(withException.allow(java.net.URI.class)); + assertFalse(withException.allow(java.net.URL.class)); + + // second compose() on the result must also preserve + final JexlPermissions withException2 = withException.compose("java.math +{}"); + assertTrue(withException2.allow(java.net.URI.class)); + assertFalse(withException2.allow(java.net.URL.class)); + + // RESTRICTED class-level deny (NOJEXL_CLASS) must survive compose that touches java.lang + final JexlPermissions restrictedPlus = JexlPermissions.RESTRICTED.compose("java.lang { +ProcessHandle { Info {} } }"); + assertFalse(restrictedPlus.allow(Thread.class)); + assertFalse(restrictedPlus.allow(Runtime.class)); + } + @Test void testComposePermissions1() throws Exception { runComposePermissions(new JexlPermissions.Delegate(JexlPermissions.UNRESTRICTED) {
