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 21dd3ca0 JEXL-381: added missing permissions checks; - allow permission compositions; 21dd3ca0 is described below commit 21dd3ca01b0cbfae500cab527abe471a39995914 Author: Henri Biestro <hbies...@cloudera.com> AuthorDate: Tue Feb 7 15:58:48 2023 +0100 JEXL-381: added missing permissions checks; - allow permission compositions; --- pom.xml | 6 ++ .../internal/introspection/DuckGetExecutor.java | 2 +- .../internal/introspection/DuckSetExecutor.java | 2 +- .../jexl3/internal/introspection/Introspector.java | 2 +- .../internal/introspection/ListGetExecutor.java | 3 +- .../internal/introspection/ListSetExecutor.java | 4 +- .../internal/introspection/MapGetExecutor.java | 3 +- .../internal/introspection/MapSetExecutor.java | 3 +- .../jexl3/internal/introspection/Permissions.java | 22 +++++- .../internal/introspection/PermissionsParser.java | 15 ++++- .../jexl3/internal/introspection/Uberspect.java | 10 +-- .../jexl3/introspection/JexlPermissions.java | 12 +++- .../commons/jexl3/ComposePermissionsTest.java | 78 ++++++++++++++++++++++ .../org/apache/commons/jexl3/JexlTestCase.java | 30 ++------- src/test/scripts/sample.json | 18 +++++ 15 files changed, 169 insertions(+), 41 deletions(-) diff --git a/pom.xml b/pom.xml index b655a9ee..4c413ecd 100644 --- a/pom.xml +++ b/pom.xml @@ -113,6 +113,12 @@ <artifactId>junit-vintage-engine</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>com.google.code.gson</groupId> + <artifactId>gson</artifactId> + <version>2.10.1</version> + <scope>test</scope> + </dependency> </dependencies> <build> diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckGetExecutor.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckGetExecutor.java index ca267d50..409bb1b6 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckGetExecutor.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckGetExecutor.java @@ -42,7 +42,7 @@ public final class DuckGetExecutor extends AbstractExecutor.Get { * @return the executor if found, null otherwise */ public static DuckGetExecutor discover(final Introspector is, final Class<?> clazz, final Object identifier) { - final java.lang.reflect.Method method = is.getMethod(clazz, "get", makeArgs(identifier)); + final java.lang.reflect.Method method = is.getMethod(clazz, "get", identifier); return method == null? null : new DuckGetExecutor(clazz, method, identifier); } diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckSetExecutor.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckSetExecutor.java index a7b35f44..d8e5d9d2 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckSetExecutor.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/DuckSetExecutor.java @@ -52,7 +52,7 @@ public final class DuckSetExecutor extends AbstractExecutor.Set { * @return the executor if found, null otherwise */ public static DuckSetExecutor discover(final Introspector is, final Class<?> clazz, final Object key, final Object value) { - java.lang.reflect.Method method = is.getMethod(clazz, "set", makeArgs(key, value)); + java.lang.reflect.Method method = is.getMethod(clazz, "set", key, value); if (method == null) { method = is.getMethod(clazz, "put", makeArgs(key, value)); } diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/Introspector.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/Introspector.java index 82945c47..1e2e417a 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/Introspector.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/Introspector.java @@ -131,7 +131,7 @@ public final class Introspector { * @return the desired method object * @throws MethodKey.AmbiguousException if no unambiguous method could be found through introspection */ - public Method getMethod(final Class<?> c, final String name, final Object[] params) { + public Method getMethod(final Class<?> c, final String name, final Object... params) { return getMethod(c, new MethodKey(name, params)); } diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/ListGetExecutor.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/ListGetExecutor.java index 7e02b5f5..866b1e67 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/ListGetExecutor.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/ListGetExecutor.java @@ -46,7 +46,8 @@ public final class ListGetExecutor extends AbstractExecutor.Get { if (clazz.isArray()) { return new ListGetExecutor(clazz, ARRAY_GET, index); } - if (List.class.isAssignableFrom(clazz)) { + // we still need to ensure permissions grant access to get(...) + if (List.class.isAssignableFrom(clazz) && is.getMethod(clazz, "get", index) != null) { return new ListGetExecutor(clazz, LIST_GET, index); } } diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/ListSetExecutor.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/ListSetExecutor.java index 4c6444b4..105b467a 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/ListSetExecutor.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/ListSetExecutor.java @@ -57,7 +57,9 @@ public final class ListSetExecutor extends AbstractExecutor.Set { return new ListSetExecutor(clazz, ARRAY_SET, index); // } } - if (List.class.isAssignableFrom(clazz)) { + // we still need to ensure permissions grant access to set(...) + if (List.class.isAssignableFrom(clazz) + && is.getMethod(clazz, "set", index, value) != null) { return new ListSetExecutor(clazz, LIST_SET, index); } } diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/MapGetExecutor.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/MapGetExecutor.java index adf80291..c620100f 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/MapGetExecutor.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/MapGetExecutor.java @@ -39,7 +39,8 @@ public final class MapGetExecutor extends AbstractExecutor.Get { * @return the executor if found, null otherwise */ public static MapGetExecutor discover(final Introspector is, final Class<?> clazz, final Object identifier) { - if (Map.class.isAssignableFrom(clazz)) { + // we still need to ensure permissions grant access to get(...) + if (Map.class.isAssignableFrom(clazz) && is.getMethod(clazz, "get", identifier) != null) { return new MapGetExecutor(clazz, MAP_GET, identifier); } return null; diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/MapSetExecutor.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/MapSetExecutor.java index e3d00811..27b80bc7 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/MapSetExecutor.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/MapSetExecutor.java @@ -43,7 +43,8 @@ public final class MapSetExecutor extends AbstractExecutor.Set { final Class<?> clazz, final Object identifier, final Object value) { - if (Map.class.isAssignableFrom(clazz)) { + // we still need to ensure permissions grant access to put(...) + if (Map.class.isAssignableFrom(clazz) && is.getMethod(clazz, "put", identifier, value) != null) { return new MapSetExecutor(clazz, MAP_SET, identifier, value); } return null; 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 ff7e3eea..d6a134dd 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 @@ -23,6 +23,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -223,6 +224,16 @@ public class Permissions implements JexlPermissions { this.packages = nojexl; } + /** + * Creates a new set of permissions by composing these permissions with a new set of rules. + * @param src the rules + * @return the new permissions + */ + @Override + public Permissions compose(final String... src) { + return new PermissionsParser().parse(new LinkedHashSet<>(allowed),new ConcurrentHashMap<>(packages), src); + } + /** * The no-restriction introspection permission singleton. */ @@ -399,15 +410,20 @@ public class Permissions implements JexlPermissions { if (deny(clazz)) { return false; } - // all super classes must be allowed + // no super class can be denied and at least one must be allowed + boolean explicit = wildcardAllow(clazz); Class<?> walk = clazz.getSuperclass(); while (walk != null) { if (deny(walk)) { return false; } + if (!explicit) { + explicit = wildcardAllow(walk); + } walk = walk.getSuperclass(); } - return true; + // check wildcards + return explicit; } /** @@ -486,7 +502,7 @@ public class Permissions implements JexlPermissions { } Class<?> clazz = method.getDeclaringClass(); // gather if any implementation of the method is explicitly allowed by the packages - final boolean[] explicit = {wildcardAllow(clazz)}; + final boolean[] explicit = { wildcardAllow(clazz) }; // lets walk all interfaces for (final Class<?> inter : clazz.getInterfaces()) { if (!allow(inter, method, explicit)) { 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 0ed25da8..ad6ec0c1 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 @@ -75,11 +75,22 @@ public class PermissionsParser { * @return the permissions map */ public Permissions parse(final String... srcs) { + return parse(new LinkedHashSet<>(), new ConcurrentHashMap<>(), srcs); + } + + /** + * Parses permissions from a source. + * @param wildcards the set of allowed packages + * @param packages the map of restricted elements + * @param srcs the sources + * @return the permissions map + */ + Permissions parse(Set<String> wildcards, Map<String, Permissions.NoJexlPackage> packages, final String... srcs) { if (srcs == null || srcs.length == 0) { return Permissions.UNRESTRICTED; } - packages = new ConcurrentHashMap<>(); - wildcards = new LinkedHashSet<>(); + this.packages = packages; + this.wildcards = wildcards; for(final String src : srcs) { this.src = src; this.size = src.length(); diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java index a7ca53d0..cb077b04 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java @@ -109,7 +109,7 @@ public class Uberspect implements JexlUberspect { protected final Introspector base() { Introspector intro = ref.get(); if (intro == null) { - // double checked locking is ok (fixed by Java 5 memory model). + // double-checked locking is ok (fixed by Java 5 memory model). synchronized (this) { intro = ref.get(); if (intro == null) { @@ -190,8 +190,7 @@ public class Uberspect implements JexlUberspect { * * @param c Class in which the method search is taking place * @param name Name of the method being searched for - * @param params An array of Objects (not Classes) that describe the - * the parameters + * @param params An array of Objects (not Classes) that describe the parameters * * @return a {@link java.lang.reflect.Method} * or null if no unambiguous method could be found through introspection. @@ -338,7 +337,7 @@ public class Uberspect implements JexlUberspect { executor = MapSetExecutor.discover(is, claz, identifier, arg); break; case LIST: - // let's see if we can convert the identifier to an int, + // let's see if we can convert the identifier to an int, // if obj is an array or a list, we can still do something final Integer index = AbstractExecutor.castInteger(identifier); if (index != null) { @@ -373,6 +372,9 @@ public class Uberspect implements JexlUberspect { @Override @SuppressWarnings("unchecked") public Iterator<?> getIterator(final Object obj) { + if (!permissions.allow(obj.getClass())) { + return null; + } if (obj instanceof Iterator<?>) { return ((Iterator<?>) obj); } 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 0ab4d64f..565f07d3 100644 --- a/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java +++ b/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java @@ -16,6 +16,7 @@ */ package org.apache.commons.jexl3.introspection; +import org.apache.commons.jexl3.internal.introspection.Permissions; import org.apache.commons.jexl3.internal.introspection.PermissionsParser; import java.lang.reflect.Constructor; @@ -168,12 +169,21 @@ public interface JexlPermissions { return new PermissionsParser().parse(src); } + /** + * Compose these permissions with a new set. + * <p>This is a convenience method meant to easily give access to the packages JEXL is + * used to integrate with.</p> + * @param src the new constraints + * @return the new permissions + */ + JexlPermissions compose(final String... src); + /** * The unrestricted permissions. * <p>This enables any public class, method, constructor or field to be visible to JEXL and used in scripts.</p> * @since 3.3 */ - JexlPermissions UNRESTRICTED = JexlPermissions.parse(null); + JexlPermissions UNRESTRICTED = Permissions.UNRESTRICTED; /** * A restricted singleton. * <p>The RESTRICTED set is built using the following allowed packages and denied packages/classes.</p> diff --git a/src/test/java/org/apache/commons/jexl3/ComposePermissionsTest.java b/src/test/java/org/apache/commons/jexl3/ComposePermissionsTest.java new file mode 100644 index 00000000..5f1a66b6 --- /dev/null +++ b/src/test/java/org/apache/commons/jexl3/ComposePermissionsTest.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3; + +import java.io.File; +import java.io.FileReader; + +import com.google.gson.Gson; +import org.apache.commons.jexl3.introspection.JexlPermissions; +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for pragmas + */ +public class ComposePermissionsTest extends JexlTestCase { + static final String SAMPLE_JSON = "src/test/scripts/sample.json"; + /** + * Create a new test case. + */ + public ComposePermissionsTest() { + super("PermissionsTest"); + } + + @Test public void testComposePermissions() throws Exception { + final String check = "http://example.com/content.jpg"; + final File jsonFile = new File(SAMPLE_JSON); + Gson gson = new Gson(); + Object json = gson.fromJson(new FileReader(jsonFile), Object.class); + Assert.assertNotNull(json); + + // will succeed because java.util.Map is allowed and gson LinkedTreeMap is one + JexlEngine j0 = createEngine(false, JexlPermissions.UNRESTRICTED); + JexlScript s0 = j0.createScript("json.pageInfo.pagePic", "json"); + Object r0 = s0.execute(null, json); + Assert.assertEquals(check, r0); + + // will fail if gson package is denied + JexlEngine j1 = createEngine(false, JexlPermissions.UNRESTRICTED.compose("com.google.gson.internal {}")); + JexlScript s1 = j1.createScript("json.pageInfo.pagePic", "json"); + try { + Object r1 = s1.execute(null, json); + Assert.fail("gson restricted"); + } catch (JexlException.Property xproperty) { + Assert.assertEquals("pageInfo", xproperty.getProperty()); + } + + // will fail since gson package is denied + j1 = createEngine(false, JexlPermissions.UNRESTRICTED.compose("com.google.gson.internal { LinkedTreeMap {} }")); + s1 = j1.createScript("json.pageInfo.pagePic", "json"); + try { + Object r1 = s1.execute(null, json); + Assert.fail("gson LinkTreeMap restricted"); + } catch (JexlException.Property xproperty) { + Assert.assertEquals("pageInfo", xproperty.getProperty()); + } + + // will not fail since gson package is not explicitely allowed + j1 = createEngine(false, JexlPermissions.RESTRICTED); + s1 = j1.createScript("json.pageInfo.pagePic", "json"); + Object r1 = s1.execute(null, json); + Assert.assertEquals(check, r0); + } +} diff --git a/src/test/java/org/apache/commons/jexl3/JexlTestCase.java b/src/test/java/org/apache/commons/jexl3/JexlTestCase.java index 62a45d7d..4ab80535 100644 --- a/src/test/java/org/apache/commons/jexl3/JexlTestCase.java +++ b/src/test/java/org/apache/commons/jexl3/JexlTestCase.java @@ -136,33 +136,15 @@ public class JexlTestCase { /** * A very secure singleton. */ - public static final JexlPermissions SECURE = JexlPermissions.parse( - "# Secure Uberspect Permissions", - "java.nio.*", - "java.io.*", - "java.lang.*", - "java.math.*", - "java.text.*", - "java.util.*", - "org.w3c.dom.*", - "org.apache.commons.jexl3.*", - "org.apache.commons.jexl3 { JexlBuilder {} }", - "org.apache.commons.jexl3.internal { Engine {} }", - "java.lang { Runtime {} System {} ProcessBuilder {} Class {} }", - "java.lang.annotation {}", - "java.lang.instrument {}", - "java.lang.invoke {}", - "java.lang.management {}", - "java.lang.ref {}", - "java.lang.reflect {}", - "java.net {}", - "java.io { File { } }", - "java.nio { Path { } Paths { } Files { } }" - ); + public static final JexlPermissions SECURE = JexlPermissions.RESTRICTED; public static JexlEngine createEngine(final boolean lenient) { + return createEngine(lenient, SECURE); + } + + public static JexlEngine createEngine(final boolean lenient, JexlPermissions permissions) { return new JexlBuilder() - .uberspect(new Uberspect(null, null, SECURE)) + .uberspect(new Uberspect(null, null, permissions)) .arithmetic(new JexlArithmetic(!lenient)).cache(128).create(); } diff --git a/src/test/scripts/sample.json b/src/test/scripts/sample.json new file mode 100644 index 00000000..3dba3077 --- /dev/null +++ b/src/test/scripts/sample.json @@ -0,0 +1,18 @@ +{ + "pageInfo": { + "pageName": "abc", + "pagePic": "http://example.com/content.jpg" + }, + "posts": [ + { + "post_id": "123456789012_123456789012", + "actor_id": "1234567890", + "picOfPersonWhoPosted": "http://example.com/photo.jpg", + "nameOfPersonWhoPosted": "Jane Doe", + "message": "Sounds cool. Can't wait to see it!", + "likesCount": "2", + "comments": [], + "timeOfPost": "1234567890" + } + ] +} \ No newline at end of file