View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl3;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertNotNull;
21  import static org.junit.jupiter.api.Assertions.assertSame;
22  import static org.junit.jupiter.api.Assertions.assertThrows;
23  
24  import org.junit.jupiter.api.AfterEach;
25  import org.junit.jupiter.api.BeforeEach;
26  import org.junit.jupiter.api.Test;
27  
28  /**
29   * Verifies that a bare {@code new JexlBuilder().create()} uses JEXL 3.7 hardened defaults:
30   * SECURE permissions and features that disable new(), global side-effects, pragmas and annotations,
31   * enable lexical scoping, and allow loops in scripts (but not in expressions).
32   *
33   * <p>This class intentionally does NOT extend JexlTestCase — JexlTestCase sets permissive
34   * defaults to keep the rest of the test suite green, which would defeat the purpose here.</p>
35   */
36  public class DefaultsTest {
37  
38      @BeforeEach
39      void restoreHardenedDefaults() {
40          // Override whatever JexlTestCase may have set when running as part of the full suite.
41          JexlBuilder.setDefaultPermissions(null);  // null → SECURE
42          JexlBuilder.setDefaultFeatures(null);     // null → hardened (no new, no global SE)
43      }
44  
45      @AfterEach
46      void restoreTestPermissive() {
47          JexlBuilder.setDefaultPermissions(JexlTestCase.TEST_PERMS);
48          JexlBuilder.setDefaultFeatures(JexlFeatures.createDefault());
49      }
50  
51      @Test
52      void testDefaultPermissionsAreSecure() {
53          final JexlBuilder builder = new JexlBuilder();
54          assertSame(org.apache.commons.jexl3.introspection.JexlPermissions.SECURE, builder.permissions());
55      }
56  
57      @Test
58      void testNewInstanceDisabledByDefault() {
59          final JexlEngine engine = new JexlBuilder().create();
60          assertThrows(JexlException.Feature.class,
61              () -> engine.createScript("new('java.util.ArrayList')"),
62              "new() must be rejected at parse time under hardened defaults");
63      }
64  
65      @Test
66      void testLoopsEnabledForScriptsNotExpressions() {
67          final JexlEngine engine = new JexlBuilder().create();
68          // loops ARE allowed in scripts
69          assertNotNull(engine.createScript("var s = 0; for (var i : [1,2,3]) { s += i; } s"),
70              "for-loop must be accepted in a script under hardened defaults");
71          // but a loop is not valid in a single expression
72          assertThrows(JexlException.class,
73              () -> engine.createExpression("for (var i : [1,2,3]) { i }"),
74              "for-loop must be rejected in an expression");
75      }
76  
77      @Test
78      void testGlobalSideEffectDisabledByDefault() {
79          final JexlEngine engine = new JexlBuilder().create();
80          assertThrows(JexlException.Feature.class,
81              () -> engine.createScript("x = 1"),
82              "global assignment must be rejected at parse time under hardened defaults");
83      }
84  
85      @Test
86      void testPragmaDisabledByDefault() {
87          final JexlEngine engine = new JexlBuilder().create();
88          assertThrows(JexlException.Feature.class,
89              () -> engine.createScript("#pragma jexl.foo 'bar'\n42"),
90              "pragma must be rejected at parse time under hardened defaults");
91      }
92  
93      @Test
94      void testAnnotationDisabledByDefault() {
95          final JexlEngine engine = new JexlBuilder().create();
96          assertThrows(JexlException.Feature.class,
97              () -> engine.createScript("@silent var x = 1"),
98              "annotation must be rejected at parse time under hardened defaults");
99      }
100 
101     @Test
102     void testLexicalEnabledByDefault() {
103         final JexlEngine engine = new JexlBuilder().create();
104         // lexical shade: a local variable cannot shadow itself / redeclare in the same scope
105         assertThrows(JexlException.class,
106             () -> engine.createScript("var x = 1; var x = 2; x"),
107             "lexical scoping must reject redeclaration under hardened defaults");
108     }
109 
110     @Test
111     void testPackageDocExampleRunsUnderSecure() {
112         // mirrors the "Brief Example" in package-info.java: must run as-is under hardened defaults
113         final JexlEngine jexl = new JexlBuilder().create();
114         final JexlExpression e = jexl.createExpression(
115             "'Hello ' + name + ', your name has ' + name.length() + ' letters'");
116         final JexlContext jc = new MapContext();
117         jc.set("name", "Aiko");
118         assertEquals("Hello Aiko, your name has 4 letters", e.evaluate(jc));
119     }
120 
121     @Test
122     void testSecureDeniesFileAndKeepsCollections() {
123         // SECURE permissions with new(...) re-enabled: still no file write, collections still work
124         final JexlEngine jexl = new JexlBuilder()
125             .permissions(org.apache.commons.jexl3.introspection.JexlPermissions.SECURE)
126             .features(JexlFeatures.createDefault())
127             .create();
128         // a Formatter cannot be constructed under SECURE - no file can be opened for writing
129         assertThrows(JexlException.class,
130             () -> jexl.createScript("new('java.util.Formatter', '/tmp/jexl-should-not-exist')").execute(null),
131             "java.util.Formatter must be denied under SECURE");
132         // collection scripting (map iteration + a view method) is intact under the tightened SECURE
133         final Object r = jexl.createScript(
134             "var m = {1 : 'a'}; var s = ''; for (var e : m) { s += e; } s + m.size()").execute(null);
135         assertEquals("a1", r);
136         // getClass() is denied even on an allowed value type: no Class can be obtained
137         assertThrows(JexlException.Method.class,
138             () -> jexl.createScript("'x'.getClass()").execute(null),
139             "getClass() must be denied under SECURE");
140     }
141 
142     @Test
143     void testSecurePermsDenyRuntime() {
144         final JexlEngine engine = new JexlBuilder().features(JexlFeatures.createDefault()).create();
145         // java.lang.Runtime is denied under SECURE
146         assertThrows(JexlException.class,
147             () -> engine.createScript("Runtime.getRuntime()").execute(new MapContext()),
148             "Runtime should be denied under SECURE permissions");
149     }
150 }