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 ".*" 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{}" +