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.internal.introspection;
18  
19  import static org.apache.commons.jexl3.introspection.JexlPermissions.RESTRICTED;
20  import static org.apache.commons.jexl3.introspection.JexlPermissions.SECURE;
21  import static org.junit.jupiter.api.Assertions.assertFalse;
22  import static org.junit.jupiter.api.Assertions.assertNotNull;
23  import static org.junit.jupiter.api.Assertions.assertNull;
24  import static org.junit.jupiter.api.Assertions.assertTrue;
25  import static org.junit.jupiter.api.Assumptions.assumeTrue;
26  
27  import java.lang.reflect.Method;
28  import java.nio.ByteBuffer;
29  import java.util.Collection;
30  import java.util.ListResourceBundle;
31  import java.util.Locale;
32  import java.util.ResourceBundle;
33  import java.util.TimeZone;
34  import java.util.Timer;
35  import java.util.TimerTask;
36  import java.util.concurrent.CompletableFuture;
37  import java.util.concurrent.Phaser;
38  import java.util.concurrent.Semaphore;
39  import java.util.stream.BaseStream;
40  
41  import org.apache.commons.jexl3.introspection.JexlUberspect;
42  import org.junit.jupiter.api.Test;
43  
44  /**
45   * Checks the RESTRICTED (and SECURE) deny-list closes the containment gaps around
46   * process handles, module/resource loading, JVM-global mutators, uninterruptible
47   * blockers and off-heap/common-pool resources.
48   */
49  class PermissionsRestrictedSweepTest {
50  
51      private static Method getMethod(final Class<?> clazz, final String name) {
52          for (final Method method : clazz.getMethods()) {
53              if (method.getName().equals(name)) {
54                  return method;
55              }
56          }
57          throw new AssertionError("Missing method: " + clazz.getName() + "#" + name);
58      }
59  
60      /** The build targets Java 8: Java 9+ classes must be referenced reflectively. */
61      private static Class<?> forName(final String name) {
62          try {
63              return Class.forName(name);
64          } catch (final ClassNotFoundException xnf) {
65              return null;
66          }
67      }
68  
69      @Test
70      void testProcessHandleDenied() {
71          // ProcessHandle can enumerate, inspect and destroy same-user OS processes (Java 9+)
72          final Class<?> processHandle = forName("java.lang.ProcessHandle");
73          assumeTrue(processHandle != null);
74          final Class<?> processHandleInfo = forName("java.lang.ProcessHandle$Info");
75          assertFalse(RESTRICTED.allow(processHandle));
76          assertFalse(RESTRICTED.allow(processHandleInfo));
77          assertFalse(RESTRICTED.allow(getMethod(processHandle, "current")));
78          assertFalse(RESTRICTED.allow(getMethod(processHandle, "allProcesses")));
79          assertFalse(RESTRICTED.allow(getMethod(processHandle, "destroyForcibly")));
80          final JexlUberspect uber = new Uberspect(null, null, RESTRICTED);
81          assertNull(uber.getClassByName("java.lang.ProcessHandle"));
82      }
83  
84      @Test
85      void testWholeClassDenialCoversNestedClasses() {
86          // -X{} must deny X$Nested as well: the classKey nests as Outer$Inner and the deny
87          // walk inherits a whole-class denial from enclosing classes
88          final Class<?> loggerFinder = forName("java.lang.System$LoggerFinder");
89          if (loggerFinder != null) {
90              assertFalse(RESTRICTED.allow(loggerFinder));
91          }
92          assertFalse(RESTRICTED.allow(ProcessBuilder.Redirect.class));
93          assertFalse(RESTRICTED.allow(Thread.State.class));
94          // control: nested classes of non-denied classes stay visible
95          assertTrue(RESTRICTED.allow(java.util.Map.Entry.class));
96      }
97  
98      @Test
99      void testModuleAndResourceLoadingDenied() {
100         // Module/ModuleLayer supply loader-free resource reading and class loading (Java 9+)
101         final Class<?> module = forName("java.lang.Module");
102         assumeTrue(module != null);
103         assertFalse(RESTRICTED.allow(module));
104         assertFalse(RESTRICTED.allow(forName("java.lang.ModuleLayer")));
105         assertFalse(RESTRICTED.allow(getMethod(module, "getResourceAsStream")));
106         // ResourceBundle.Control.newBundle re-opens property-file reading and class loading
107         assertFalse(RESTRICTED.allow(ResourceBundle.Control.class));
108         assertFalse(RESTRICTED.allow(getMethod(ResourceBundle.Control.class, "newBundle")));
109         // SECURE parity: RESTRICTED denies ListResourceBundle too
110         assertFalse(RESTRICTED.allow(ListResourceBundle.class));
111         // control: ResourceBundle class itself stays visible, only its loader members are denied
112         assertFalse(RESTRICTED.allow(getMethod(ResourceBundle.class, "getBundle")));
113         assertTrue(RESTRICTED.allow(getMethod(ResourceBundle.class, "getString")));
114     }
115 
116     @Test
117     void testGlobalMutatorsDenied() {
118         // Locale.setDefault/TimeZone.setDefault flip JVM-process-global defaults
119         for (final org.apache.commons.jexl3.introspection.JexlPermissions p
120                 : new org.apache.commons.jexl3.introspection.JexlPermissions[] { RESTRICTED, SECURE }) {
121             assertFalse(p.allow(getMethod(Locale.class, "setDefault")));
122             assertFalse(p.allow(getMethod(TimeZone.class, "setDefault")));
123         }
124         // controls: the read-only side stays visible
125         assertTrue(RESTRICTED.allow(getMethod(Locale.class, "getDefault")));
126         assertTrue(RESTRICTED.allow(getMethod(TimeZone.class, "getDefault")));
127     }
128 
129     @Test
130     void testUninterruptibleBlockersDenied() {
131         // interrupt-immune waits defeat the documented cancellation mitigation
132         assertFalse(RESTRICTED.allow(getMethod(CompletableFuture.class, "join")));
133         assertFalse(RESTRICTED.allow(getMethod(Semaphore.class, "acquireUninterruptibly")));
134         assertFalse(RESTRICTED.allow(getMethod(Phaser.class, "awaitAdvance")));
135         assertFalse(RESTRICTED.allow(getMethod(Phaser.class, "arriveAndAwaitAdvance")));
136         // controls: the interruptible counterparts stay visible
137         assertTrue(RESTRICTED.allow(getMethod(CompletableFuture.class, "get")));
138         assertTrue(RESTRICTED.allow(getMethod(Semaphore.class, "acquire")));
139         assertTrue(RESTRICTED.allow(getMethod(Phaser.class, "awaitAdvanceInterruptibly")));
140         // Timer starts a non-daemon thread that outlives the evaluation
141         assertFalse(RESTRICTED.allow(Timer.class));
142         assertFalse(RESTRICTED.allow(TimerTask.class));
143     }
144 
145     @Test
146     void testResourceBurnDenied() {
147         // common fork-join pool burn and off-heap allocation
148         assertFalse(RESTRICTED.allow(getMethod(BaseStream.class, "parallel")));
149         assertFalse(RESTRICTED.allow(getMethod(Collection.class, "parallelStream")));
150         assertFalse(RESTRICTED.allow(getMethod(ByteBuffer.class, "allocateDirect")));
151         // controls: sequential streaming and heap allocation stay visible
152         assertTrue(RESTRICTED.allow(getMethod(BaseStream.class, "sequential")));
153         assertTrue(RESTRICTED.allow(getMethod(Collection.class, "stream")));
154         assertTrue(RESTRICTED.allow(getMethod(ByteBuffer.class, "allocate")));
155         final Method spliterator = getMethod(Collection.class, "spliterator");
156         assertNotNull(spliterator);
157         assertTrue(RESTRICTED.allow(spliterator));
158     }
159 }