This is an automated email from the ASF dual-hosted git repository.

henrib pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-jexl.git


The following commit(s) were added to refs/heads/master by this push:
     new fe73af4d JEXL-462 : close RESTRICTED permission set; fix wildcard 
exposure - Replace all '.*' wildcards in RESTRICTED with explicit '+{}' package 
declarations so future JDK subpackages are never silently admitted. - Add 
missing safe packages (java.uti.function/stream/regex/concurrent.atomic, 
java.time.chrono/format/temporal/zone) and deny known hazards 
(ZoneRulesProvider, executor classes, java.util.zip/jar/prefs/logging via the 
closed-world boundary). - Extend the permissions en [...]
fe73af4d is described below

commit fe73af4d65c9b4c1bbe90b4951d6d10b75eb8053
Author: Henrib <[email protected]>
AuthorDate: Wed Jun 24 17:50:01 2026 +0200

    JEXL-462 : close RESTRICTED permission set; fix wildcard exposure
    - Replace all '.*' wildcards in RESTRICTED with explicit '+{}' package 
declarations so future JDK subpackages are never silently admitted.
    - Add missing safe packages 
(java.uti.function/stream/regex/concurrent.atomic, 
java.time.chrono/format/temporal/zone) and deny known hazards 
(ZoneRulesProvider, executor classes, java.util.zip/jar/prefs/logging via the 
closed-world boundary).
    - Extend the permissions engine: any explicit package declaration (positive 
or negative) now closes the world — only declared packages are accessible.
    - NoJexlPackage gains hasAllowedClass() to distinguish a deny-list 
(unlisted class = allowed) from an allow-list (unlisted class = denied), 
matching the semantics of 'java.lang { Runtime {} }' vs 'java.io -{ 
+PrintWriter{} }'.
---
 .../jexl3/internal/introspection/Permissions.java  | 52 ++++++++++++++---
 .../internal/introspection/PermissionsParser.java  |  7 +++
 .../jexl3/introspection/JexlPermissions.java       | 68 ++++++++++++++--------
 3 files changed, 97 insertions(+), 30 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 b5d15988..47ab2b8e 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
@@ -172,6 +172,22 @@ public class Permissions implements JexlPermissions {
 
         boolean isEmpty() { return nojexl.isEmpty(); }
 
+        /**
+         * Whether this package has at least one explicitly-allowed class (a 
{@code JexlClass} entry).
+         * <p>A package with allowed-class entries acts as an allow-list: 
unlisted classes are denied.
+         * A package with only denied-class entries acts as a deny-list: 
unlisted classes are allowed.</p>
+         *
+         * @return true if at least one class is explicitly allowed
+         */
+        boolean hasAllowedClass() {
+            for (final NoJexlClass njc : nojexl.values()) {
+                if (njc instanceof JexlClass) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
         @Override public NoJexlPackage copy() {
             return new NoJexlPackage(copyMap(nojexl));
         }
@@ -250,13 +266,18 @@ public class Permissions implements JexlPermissions {
      */
     private final Map<String, NoJexlPackage> packages;
     /**
-     * The closed world package patterns.
+     * The allowed package patterns (wildcards or exact package names).
+     * <p>Empty together with an empty {@link #packages} map means open-world: 
every package is accessible
+     * and only explicitly denied elements are carved out — the behavior of 
{@link #UNRESTRICTED}.
+     * Empty with a non-empty {@link #packages} map, or non-empty, means 
closed-world: only declared
+     * packages are accessible.</p>
      */
     private final Set<String> allowed;
 
     /** Allow inheritance. */
     protected Permissions() {
-        this(Collections.emptySet(), Collections.emptyMap());
+        this.allowed = Collections.emptySet();
+        this.packages = Collections.emptyMap();
     }
 
     /**
@@ -361,6 +382,21 @@ public class Permissions implements JexlPermissions {
         return allowed == null ? Collections.emptySet() : 
Collections.unmodifiableSet(allowed);
     }
 
+    /**
+     * Whether a package belongs to the allowed perimeter.
+     * <p>Open-world ({@link #UNRESTRICTED}: no rules at all) allows every 
package. Closed-world requires the
+     * package to match an entry in {@link #allowed}; an empty perimeter in 
closed-world matches nothing.</p>
+     *
+     * @param packageName the package name (not null)
+     * @return true if allowed, false otherwise
+     */
+    private boolean allowedPackage(final String packageName) {
+        if (allowed.isEmpty() && packages.isEmpty()) {
+            return true;
+        }
+        return !allowed.isEmpty() && wildcardAllow(allowed, packageName);
+    }
+
     /**
      * Whether the wildcard set of packages allows a given class to be 
introspected.
      *
@@ -368,7 +404,7 @@ public class Permissions implements JexlPermissions {
      * @return true if allowed, false otherwise
      */
     private boolean wildcardAllow(final Class<?> clazz) {
-        return wildcardAllow(allowed, ClassTool.getPackageName(clazz));
+        return allowedPackage(ClassTool.getPackageName(clazz));
     }
 
     /**
@@ -384,19 +420,21 @@ public class Permissions implements JexlPermissions {
      */
     private <T> boolean specifiedAllow(final Class<?> clazz, T name, 
BiPredicate<NoJexlClass, T> check) {
         final String packageName = ClassTool.getPackageName(clazz);
-        if (wildcardAllow(allowed, packageName)) {
+        if (allowedPackage(packageName)) {
             return true;
         }
         final NoJexlPackage njp = packages.get(packageName);
         if (njp != null && check != null) {
             // there is a package permission, check if there is a class 
permission
             final NoJexlClass njc = njp.getNoJexl(clazz);
-            // if there is a class permission, perform the check
             if (njc != null) {
                 return check.test(njc, name);
             }
+            // class not listed: allowed if the package is a deny-list (no 
explicit class allows);
+            // denied if the package is an allow-list (e.g. java.io -{ 
+PrintWriter{} ... })
+            return !njp.hasAllowedClass();
         }
-        // nothing explicit
+        // package not declared at all
         return false;
     }
 
@@ -627,7 +665,7 @@ public class Permissions implements JexlPermissions {
         // an explicit package entry is allowed unless it is the deny marker
         final String name = pack.getName();
         final NoJexlPackage njp = packages.get(name);
-        return njp == null ? wildcardAllow(allowed, name) : 
!Objects.equals(NOJEXL_PACKAGE, njp);
+        return njp == null ? allowedPackage(name) : 
!Objects.equals(NOJEXL_PACKAGE, njp);
     }
 
 
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 3b163df2..629d0a0b 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
@@ -381,6 +381,13 @@ public class PermissionsParser {
                     packages.put(pname, negative == null || negative
                         ? Permissions.NOJEXL_PACKAGE
                         : Permissions.JEXL_PACKAGE);
+                    // a wholly-allowed package (pkg +{}) joins the allowed 
perimeter as an exact match (no '.*').
+                    // This lets a closed set of packages be declared without 
wildcards - so future sub-packages are
+                    // never implicitly allowed - while still allowing types 
based on this package (e.g. a foreign
+                    // Map implementation extending java.util.AbstractMap) the 
same way a '.*' wildcard would.
+                    if (Boolean.FALSE.equals(negative)) {
+                        wildcards.add(pname);
+                    }
                 } else {
                     packages.put(pname, njpackage);
                 }
diff --git 
a/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java 
b/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java
index 5db8a848..b204de6d 100644
--- a/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java
+++ b/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java
@@ -273,36 +273,58 @@ public interface JexlPermissions {
      * and its host.
      * </p>
      * <p>
-     * As a simple guide, any line that ends with &quot;.*&quot; is allowing a 
package, any other is
-     * denying a package, class or method.
+     * Every allowed package is declared explicitly using the positive {@code 
+{}} syntax rather than a
+     * {@code .*} wildcard. A wildcard matches a package <em>and all of its 
sub-packages</em>, which is not
+     * future-proof: a sub-package added by a later JDK (or a dangerous 
existing one such as
+     * {@code java.util.zip}/{@code java.util.jar} - which can read files - or 
{@code java.nio.file}) would be
+     * silently exposed. Listing each package explicitly keeps the perimeter 
closed: only the packages below are
+     * visible, nothing else.
      * </p>
+     * <p>Allowed packages (each member is visible unless explicitly 
denied):</p>
      * <ul>
-     * <li>java.nio.*</li>
-     * <li>java.lang.*</li>
-     * <li>java.math.*</li>
-     * <li>java.text.*</li>
-     * <li>java.util.*</li>
-     * <li>org.w3c.dom.*</li>
-     * <li>org.apache.commons.jexl3.*</li>
-     *
-     * <li>org.apache.commons.jexl3 { JexlBuilder {} }</li>
-     * <li>org.apache.commons.jexl3.introspection { JexlPermissions {} 
JexlPermissions$ClassPermissions {} }</li>
-     * <li>org.apache.commons.jexl3.internal { Engine {} Engine32 {} 
TemplateEngine {} }</li>
-     * <li>org.apache.commons.jexl3.internal.introspection { Uberspect {} 
Introspector {} }</li>
-     * <li>java.lang { Runtime {} System {} ProcessBuilder {} Process {} 
RuntimePermission {} SecurityManager {} Thread {} ThreadGroup {} Class {} }</li>
-     * <li>java.io { +PrintWriter {} +Writer {} +StringWriter {} +Reader {} 
+InputStream {} +OutputStream {} }</li>
-     * <li>java.nio +{}</li>
-     * <li>java.rmi {}</li>
+     * <li>java.math</li>
+     * <li>java.text</li>
+     * <li>java.time, java.time.chrono, java.time.format, java.time.temporal, 
java.time.zone</li>
+     * <li>java.util, java.util.concurrent, java.util.concurrent.atomic, 
java.util.function, java.util.stream, java.util.regex</li>
+     * <li>java.nio, java.nio.charset</li>
+     * <li>org.w3c.dom</li>
+     * <li>java.lang (minus the denied classes below)</li>
+     * <li>org.apache.commons.jexl3 (minus JexlBuilder)</li>
+     * </ul>
+     * <p>Denied classes / members (carved out of otherwise-allowed 
packages):</p>
+     * <ul>
+     * <li>java.lang { Runtime, System, ProcessBuilder, Process, 
RuntimePermission, SecurityManager, Thread, ThreadGroup, Class, ClassLoader 
}</li>
+     * <li>java.io { everything except PrintWriter, Writer, StringWriter, 
Reader, InputStream, OutputStream }</li>
+     * <li>java.util.concurrent { Executors and the thread-pool / fork-join 
executor classes }</li>
+     * <li>java.time.zone { ZoneRulesProvider } (prevents JVM-wide time-zone 
provider registration)</li>
+     * <li>org.apache.commons.jexl3 { JexlBuilder }</li>
      * </ul>
+     * <p>Notably absent (and therefore denied) are 
file/IO/persistence/loader-bearing packages such as
+     * {@code java.util.zip}, {@code java.util.jar}, {@code java.util.prefs}, 
{@code java.util.logging},
+     * {@code java.util.concurrent.locks}, {@code java.nio.file}, {@code 
java.lang.reflect},
+     * {@code java.lang.invoke} and {@code org.w3c.dom.ls}.</p>
      */
 
     JexlPermissions RESTRICTED = JexlPermissions.parse(
         "# Default Uberspect Permissions",
-        "java.math.*",
-        "java.text.*",
-        "java.time.*",
-        "java.util.*",
-        "org.w3c.dom.*",
+        "java.math +{}",
+        "java.text +{}",
+        "java.time +{}",
+        "java.time.chrono +{}",
+        "java.time.format +{}",
+        "java.time.temporal +{}",
+        "java.time.zone +{ -ZoneRulesProvider{} }",
+        "java.util +{}",
+        "java.util.concurrent +{" +
+            "-Executors{} -ExecutorService{} -AbstractExecutorService{}" +
+            "-ThreadPoolExecutor{} -ScheduledThreadPoolExecutor{} 
-ScheduledExecutorService{}" +
+            "-ForkJoinPool{} -ForkJoinTask{} -ForkJoinWorkerThread{}" +
+            "}",
+        "java.util.concurrent.atomic +{}",
+        "java.util.function +{}",
+        "java.util.stream +{}",
+        "java.util.regex +{}",
+        "org.w3c.dom +{}",
         "java.lang +{" +
             "-Runtime{} -System{} -ProcessBuilder{} -Process{}" +
             "-RuntimePermission{} -SecurityManager{}" +

Reply via email to