This is an automated email from the ASF dual-hosted git repository. henrib pushed a commit to branch JEXL-384 in repository https://gitbox.apache.org/repos/asf/commons-jexl.git
commit 913608a807f296f4501f1e1be0d3bcd45f03ab27 Author: henrib <hen...@apache.org> AuthorDate: Fri Nov 4 17:10:27 2022 +0100 JEXL-384: enforce strictness of operators by checking null arguments in all operator calls; --- .../org/apache/commons/jexl3/JexlArithmetic.java | 13 +- .../apache/commons/jexl3/internal/Operators.java | 30 +++- .../org/apache/commons/jexl3/Issues300Test.java | 178 +++++++++++++++++++-- .../java/org/apache/commons/jexl3/JexlTest.java | 16 +- 4 files changed, 211 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java index 4610d227..e813151a 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java +++ b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java @@ -379,7 +379,8 @@ public class JexlArithmetic { * <p>When an operator is strict, it does <em>not</em> accept null arguments when the arithmetic is strict. * If null-safe (ie not-strict), the operator does accept null arguments even if the arithmetic itself is strict.</p> * <p>The default implementation considers equal/not-equal operators as null-safe so one can check for null as in - * <code>if (myvar == null) {...}</code>. Note that this operator is used for equal and not-equal syntax.</p> + * <code>if (myvar == null) {...}</code>. Note that this operator is used for equal and not-equal syntax. The complete + * list of operators that are not strict are (==, [], []=, ., .=). </p> * <p>An arithmetic refining its strict behavior handling for more operators must declare which by overriding * this method.</p> * @param operator the operator to check for null-argument(s) handling @@ -387,7 +388,15 @@ public class JexlArithmetic { * for null argument(s) */ public boolean isStrict(JexlOperator operator) { - return operator == JexlOperator.EQ? false : isStrict(); + switch(operator) { + case EQ: + case ARRAY_GET: + case ARRAY_SET: + case PROPERTY_GET: + case PROPERTY_SET: + return false; + } + return isStrict(); } /** diff --git a/src/main/java/org/apache/commons/jexl3/internal/Operators.java b/src/main/java/org/apache/commons/jexl3/internal/Operators.java index f0da705e..b81d1540 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Operators.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Operators.java @@ -88,18 +88,44 @@ public class Operators { return false; } + /** + * Throw a NPE if operator is strict and one of the arguments is null. + * @param arithmetic the JEXL arithmetic instance + * @param operator the operator to check + * @param args the operands + * @throws JexlArithmetic.NullOperand if operator is strict and an operand is null + */ + protected void controlNullOperands(JexlArithmetic arithmetic, JexlOperator operator, Object...args) { + for (Object arg : args) { + // only check operator if necessary + if (arg == null) { + // check operator only once if it is not strict + if (arithmetic.isStrict(operator)) { + throw new JexlArithmetic.NullOperand(); + } else { + break; + } + } + } + } + /** * Attempts to call an operator. * <p> - * This takes care of finding and caching the operator method when appropriate + * This performs the null argument control against the strictness of the operator. + * </p> + * <p> + * This takes care of finding and caching the operator method when appropriate. + * </p> * @param node the syntactic node * @param operator the operator * @param args the arguments * @return the result of the operator evaluation or TRY_FAILED */ protected Object tryOverload(final JexlNode node, final JexlOperator operator, Object... args) { + final JexlArithmetic arithmetic = interpreter.arithmetic; + controlNullOperands(arithmetic, operator, args); if (operators != null && operators.overloads(operator)) { - final JexlArithmetic arithmetic = interpreter.arithmetic; final boolean cache = interpreter.cache; try { if (cache) { diff --git a/src/test/java/org/apache/commons/jexl3/Issues300Test.java b/src/test/java/org/apache/commons/jexl3/Issues300Test.java index 78a799e7..f80ac72d 100644 --- a/src/test/java/org/apache/commons/jexl3/Issues300Test.java +++ b/src/test/java/org/apache/commons/jexl3/Issues300Test.java @@ -41,7 +41,7 @@ import static org.junit.Assert.assertEquals; */ public class Issues300Test { @Test - public void testIssue301a() { + public void test301a() { final JexlEngine jexl = new JexlBuilder().safe(false).arithmetic(new JexlArithmetic(false)).create(); final String[] srcs = new String[]{ "var x = null; x.0", "var x = null; x[0]", "var x = [null,1]; x[0][0]" @@ -61,7 +61,7 @@ public class Issues300Test { } @Test - public void testIssues301b() { + public void tests301b() { final JexlEngine jexl = new JexlBuilder().safe(false).arithmetic(new JexlArithmetic(false)).create(); final Object[] xs = new Object[]{null, null, new Object[]{null, 1}}; final String[] srcs = new String[]{ @@ -82,7 +82,7 @@ public class Issues300Test { } @Test - public void testIssue302() { + public void test302() { final JexlContext jc = new MapContext(); final String[] strs = new String[]{ "{if (0) 1 else 2; var x = 4;}", @@ -100,7 +100,7 @@ public class Issues300Test { } @Test - public void testIssue304() { + public void test304() { final JexlEngine jexlEngine = new JexlBuilder().strict(false).create(); JexlExpression e304 = jexlEngine.createExpression("overview.limit.var"); @@ -150,7 +150,7 @@ public class Issues300Test { } @Test - public void testIssue305() { + public void test305() { final JexlEngine jexl = new JexlBuilder().create(); JexlScript e; e = jexl.createScript("{while(false) {}; var x = 1;}"); @@ -162,7 +162,7 @@ public class Issues300Test { } @Test - public void testIssue306() { + public void test306() { final JexlContext ctxt = new MapContext(); final JexlEngine jexl = new JexlBuilder().create(); final JexlScript e = jexl.createScript("x.y ?: 2"); @@ -174,7 +174,7 @@ public class Issues300Test { } @Test - public void testIssue306a() { + public void test306a() { final JexlEngine jexl = new JexlBuilder().create(); final JexlScript e = jexl.createScript("x.y ?: 2", "x"); Object o = e.execute(null, new Object()); @@ -184,7 +184,7 @@ public class Issues300Test { } @Test - public void testIssue306b() { + public void test306b() { final JexlEngine jexl = new JexlBuilder().create(); final JexlScript e = jexl.createScript("x?.y ?: 2", "x"); final Object o1 = e.execute(null, new Object()); @@ -194,7 +194,7 @@ public class Issues300Test { } @Test - public void testIssue306c() { + public void test306c() { final JexlEngine jexl = new JexlBuilder().safe(true).create(); final JexlScript e = jexl.createScript("x.y ?: 2", "x"); Object o = e.execute(null, new Object()); @@ -204,7 +204,7 @@ public class Issues300Test { } @Test - public void testIssue306d() { + public void test306d() { final JexlEngine jexl = new JexlBuilder().safe(true).create(); final JexlScript e = jexl.createScript("x.y[z.t] ?: 2", "x"); Object o = e.execute(null, new Object()); @@ -214,7 +214,7 @@ public class Issues300Test { } @Test - public void testIssue309a() { + public void test309a() { final String src = "<html lang=\"en\">\n" + " <body>\n" + " <h1>Hello World!</h1>\n" @@ -233,7 +233,7 @@ public class Issues300Test { } @Test - public void testIssue309b() { + public void test309b() { final String src = "<html lang=\"en\">\n" + " <body>\n" + " <h1>Hello World!</h1>\n" @@ -252,7 +252,7 @@ public class Issues300Test { } @Test - public void testIssue309c() { + public void test309c() { final String src = "<html lang=\"en\">\n" + " <body>\n" + " <h1>Hello World!</h1>\n" @@ -715,7 +715,7 @@ public class Issues300Test { @Test public void test361b_32() { - JexlEngine jexl = new Engine32(new JexlBuilder().safe(false)); + JexlEngine jexl = new Engine32(new JexlBuilder().safe(false).strict(false)); Object result = run361b(jexl); Assert.assertNotNull(result); } @@ -748,7 +748,7 @@ public class Issues300Test { @Test public void test361c_32() { - JexlEngine jexl = new Engine32(new JexlBuilder().safe(false)); + JexlEngine jexl = new Engine32(new JexlBuilder().safe(false).strict(false)); String result = run361c(jexl); Assert.assertNotNull(result); } @@ -968,7 +968,7 @@ public class Issues300Test { public static class Context0930 extends MapContext { /** - * This allows using a JEXL lmabda as a filter. + * This allows using a JEXL lambda as a filter. * @param stream the stream * @param filter the lambda to use as filter * @return the filtered stream @@ -979,7 +979,7 @@ public class Issues300Test { } @Test - public void testStackOvflw20220930() { + public void testSO20220930() { // fill some drivers in a list List<Driver0930> values = new ArrayList<>(); for(int i = 0; i < 8; ++i) { @@ -1010,4 +1010,148 @@ public class Issues300Test { } } } + + public static class Arithmetic383 extends JexlArithmetic { + public Arithmetic383(boolean astrict) { + super(astrict); + } + @Override + public boolean toBoolean(final Object val) { + return val == null? false : super.toBoolean(val); + } + @Override + public Object not(final Object val) { + return val == null? true : super.not(val); + } + @Override + public boolean isStrict(JexlOperator op) { + if (JexlOperator.NOT == op) { + return false; + } + return super.isStrict(op); + } + } + + @Test public void test383() { + JexlEngine jexl = new JexlBuilder().safe(false).arithmetic(new Arithmetic383(true)).create(); + String src0 = "if (a) 1; else 2;"; + String src1 = "if (!a) 1; else 2;"; + // local var + JexlScript s0 = jexl.createScript(src0, "a"); + JexlScript s1 = jexl.createScript(src1, "a"); + Assert.assertEquals(2, s0.execute(null, null)); + Assert.assertEquals(1, s1.execute(null, null)); + // global var undefined + s0 = jexl.createScript(src0); + s1 = jexl.createScript(src1); + try { + Assert.assertEquals(2, s0.execute(null, null)); + } catch(JexlException.Variable xvar) { + Assert.assertEquals("a", xvar.getVariable()); + } + try { + Assert.assertEquals(1, s1.execute(null, null)); + } catch(JexlException.Variable xvar) { + Assert.assertEquals("a", xvar.getVariable()); + } + // global var null + MapContext ctxt = new MapContext(); + ctxt.set("a", null); + Assert.assertEquals(2, s0.execute(ctxt, null)); + Assert.assertEquals(1, s1.execute(ctxt, null)); + } + + public static class Arithmetic384 extends JexlArithmetic { + public Arithmetic384(boolean astrict) { + super(astrict); + } + + @Override + public boolean isStrict(JexlOperator op) { + if (JexlOperator.ADD == op) { + return false; + } + return super.isStrict(op); + } + } + @Test + public void test384a() { + JexlEngine jexl = new JexlBuilder() + .safe(false) + .strict(true) + .create(); + // constant + for(String src0 : Arrays.asList("'ABC' + null", "null + 'ABC'")) { + JexlContext ctxt = new MapContext(); + JexlScript s0 = jexl.createScript(src0); + try { + s0.execute(ctxt, null); + Assert.fail("null argument should throw"); + } catch (JexlException xvar) { + Assert.assertTrue(xvar.toString().contains("+")); + } + } + // null local a + for(String src1 : Arrays.asList("'ABC' + a", "a + 'ABC'")) { + JexlContext ctxt = new MapContext(); + JexlScript s1 = jexl.createScript(src1, "a"); + try { + s1.execute(ctxt, null); + Assert.fail("null argument should throw"); + } catch (JexlException.Variable xvar) { + Assert.assertEquals("a", xvar.getVariable()); + } + // undefined a + s1 = jexl.createScript(src1); + try { + s1.execute(ctxt, null); + Assert.fail("null argument should throw"); + } catch (JexlException.Variable xvar) { + Assert.assertEquals("a", xvar.getVariable()); + Assert.assertTrue(xvar.isUndefined()); + } + // null a + ctxt.set("a", null); + try { + s1.execute(ctxt, null); + Assert.fail("null argument should throw"); + } catch (JexlException.Variable xvar) { + Assert.assertEquals("a", xvar.getVariable()); + Assert.assertFalse(xvar.isUndefined()); + } + } + } + @Test + public void test384b() { + // be explicit about + handling null + JexlEngine jexl = new JexlBuilder() + .arithmetic(new Arithmetic384(true)) + .safe(false) + .strict(true) + .create(); + // constant + for(String src0 : Arrays.asList("'ABC' + null", "null + 'ABC'")) { + JexlContext ctxt = new MapContext(); + JexlScript s0 = jexl.createScript(src0); + Assert.assertEquals("ABC", s0.execute(ctxt)); + } + // null local a + for(String src1 : Arrays.asList("'ABC' + a", "a + 'ABC'")) { + JexlContext ctxt = new MapContext(); + JexlScript s1 = jexl.createScript(src1, "a"); + Assert.assertEquals("ABC", s1.execute(ctxt, null)); + // undefined a + s1 = jexl.createScript(src1); + try { + s1.execute(ctxt, null); + Assert.fail("null argument should throw"); + } catch (JexlException.Variable xvar) { + Assert.assertEquals("a", xvar.getVariable()); + Assert.assertTrue(xvar.isUndefined()); + } + // null a + ctxt.set("a", null); + Assert.assertEquals("ABC", s1.execute(ctxt, null)); + } + } } diff --git a/src/test/java/org/apache/commons/jexl3/JexlTest.java b/src/test/java/org/apache/commons/jexl3/JexlTest.java index dcde99c3..11c11f2d 100644 --- a/src/test/java/org/apache/commons/jexl3/JexlTest.java +++ b/src/test/java/org/apache/commons/jexl3/JexlTest.java @@ -19,6 +19,7 @@ package org.apache.commons.jexl3; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; +import java.util.Arrays; import java.util.BitSet; import java.util.Calendar; import java.util.GregorianCalendar; @@ -39,12 +40,17 @@ import org.junit.Test; * @since 1.0 */ @SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"}) -public class JexlTest extends JexlTestCase { - protected static final String METHOD_STRING = "Method string"; - protected static final String GET_METHOD_STRING = "GetMethod string"; +public final class JexlTest extends JexlTestCase { + static final String METHOD_STRING = "Method string"; + static final String GET_METHOD_STRING = "GetMethod string"; public JexlTest() { - super("JexlTest"); + super("JexlTest", + new JexlBuilder() + .strict(false) + .imports(Arrays.asList("java.lang","java.math")) + .permissions(null) + .cache(128).create()); } /** @@ -158,7 +164,7 @@ public class JexlTest extends JexlTestCase { options.setStrict(false); jc.set("string", ""); jc.set("array", new Object[0]); - jc.set("map", new HashMap<Object, Object>()); + jc.set("map", new HashMap<>()); jc.set("list", new ArrayList<Object>()); jc.set("set", (new HashMap<Object, Object>()).keySet()); jc.set("longstring", "thingthing");