|
| 1 | +/* |
| 2 | + * Licensed to Elasticsearch under one or more contributor |
| 3 | + * license agreements. See the NOTICE file distributed with |
| 4 | + * this work for additional information regarding copyright |
| 5 | + * ownership. Elasticsearch licenses this file to you under |
| 6 | + * the Apache License, Version 2.0 (the "License"); you may |
| 7 | + * not use this file except in compliance with the License. |
| 8 | + * You may obtain a copy of the License at |
| 9 | + * |
| 10 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | + * |
| 12 | + * Unless required by applicable law or agreed to in writing, |
| 13 | + * software distributed under the License is distributed on an |
| 14 | + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | + * KIND, either express or implied. See the License for the |
| 16 | + * specific language governing permissions and limitations |
| 17 | + * under the License. |
| 18 | + */ |
| 19 | + |
| 20 | +package org.elasticsearch.secure_sm; |
| 21 | + |
| 22 | +import java.security.AccessController; |
| 23 | +import java.security.Permission; |
| 24 | +import java.security.PrivilegedAction; |
| 25 | +import java.util.Objects; |
| 26 | + |
| 27 | +/** |
| 28 | + * Extension of SecurityManager that works around a few design flaws in Java Security. |
| 29 | + * <p> |
| 30 | + * There are a few major problems that require custom {@code SecurityManager} logic to fix: |
| 31 | + * <ul> |
| 32 | + * <li>{@code exitVM} permission is implicitly granted to all code by the default |
| 33 | + * Policy implementation. For a server app, this is not wanted. </li> |
| 34 | + * <li>ThreadGroups are not enforced by default, instead only system threads are |
| 35 | + * protected out of box by {@code modifyThread/modifyThreadGroup}. Applications |
| 36 | + * are encouraged to override the logic here to implement a stricter policy. |
| 37 | + * <li>System threads are not even really protected, because if the system uses |
| 38 | + * ThreadPools, {@code modifyThread} is abused by its {@code shutdown} checks. This means |
| 39 | + * a thread must have {@code modifyThread} to even terminate its own pool, leaving |
| 40 | + * system threads unprotected. |
| 41 | + * </ul> |
| 42 | + * This class throws exception on {@code exitVM} calls, and provides a whitelist where calls |
| 43 | + * from exit are allowed. |
| 44 | + * <p> |
| 45 | + * Additionally it enforces threadgroup security with the following rules: |
| 46 | + * <ul> |
| 47 | + * <li>{@code modifyThread} and {@code modifyThreadGroup} are required for any thread access |
| 48 | + * checks: with these permissions, access is granted as long as the thread group is |
| 49 | + * the same or an ancestor ({@code sourceGroup.parentOf(targetGroup) == true}). |
| 50 | + * <li>code without these permissions can do very little, except to interrupt itself. It may |
| 51 | + * not even create new threads. |
| 52 | + * <li>very special cases (like test runners) that have {@link ThreadPermission} can violate |
| 53 | + * threadgroup security rules. |
| 54 | + * </ul> |
| 55 | + * <p> |
| 56 | + * If java security debugging ({@code java.security.debug}) is enabled, and this SecurityManager |
| 57 | + * is installed, it will emit additional debugging information when threadgroup access checks fail. |
| 58 | + * |
| 59 | + * @see SecurityManager#checkAccess(Thread) |
| 60 | + * @see SecurityManager#checkAccess(ThreadGroup) |
| 61 | + * @see <a href="http://cs.oswego.edu/pipermail/concurrency-interest/2009-August/006508.html"> |
| 62 | + * http://cs.oswego.edu/pipermail/concurrency-interest/2009-August/006508.html</a> |
| 63 | + */ |
| 64 | +public class SecureSM extends SecurityManager { |
| 65 | + |
| 66 | + private final String[] classesThatCanExit; |
| 67 | + |
| 68 | + /** |
| 69 | + * Creates a new security manager where no packages can exit nor halt the virtual machine. |
| 70 | + */ |
| 71 | + public SecureSM() { |
| 72 | + this(new String[0]); |
| 73 | + } |
| 74 | + |
| 75 | + /** |
| 76 | + * Creates a new security manager with the specified list of regular expressions as the those that class names will be tested against to |
| 77 | + * check whether or not a class can exit or halt the virtual machine. |
| 78 | + * |
| 79 | + * @param classesThatCanExit the list of classes that can exit or halt the virtual machine |
| 80 | + */ |
| 81 | + public SecureSM(final String[] classesThatCanExit) { |
| 82 | + this.classesThatCanExit = classesThatCanExit; |
| 83 | + } |
| 84 | + |
| 85 | + /** |
| 86 | + * Creates a new security manager with a standard set of test packages being the only packages that can exit or halt the virtual |
| 87 | + * machine. The packages that can exit are: |
| 88 | + * <ul> |
| 89 | + * <li><code>org.apache.maven.surefire.booter.</code></li> |
| 90 | + * <li><code>com.carrotsearch.ant.tasks.junit4.</code></li> |
| 91 | + * <li><code>org.eclipse.internal.junit.runner.</code></li> |
| 92 | + * <li><code>com.intellij.rt.execution.junit.</code></li> |
| 93 | + * </ul> |
| 94 | + * |
| 95 | + * @return an instance of SecureSM where test packages can halt or exit the virtual machine |
| 96 | + */ |
| 97 | + public static SecureSM createTestSecureSM() { |
| 98 | + return new SecureSM(TEST_RUNNER_PACKAGES); |
| 99 | + } |
| 100 | + |
| 101 | + static final String[] TEST_RUNNER_PACKAGES = new String[] { |
| 102 | + // surefire test runner |
| 103 | + "org\\.apache\\.maven\\.surefire\\.booter\\..*", |
| 104 | + // junit4 test runner |
| 105 | + "com\\.carrotsearch\\.ant\\.tasks\\.junit4\\.slave\\..*", |
| 106 | + // eclipse test runner |
| 107 | + "org\\.eclipse.jdt\\.internal\\.junit\\.runner\\..*", |
| 108 | + // intellij test runner |
| 109 | + "com\\.intellij\\.rt\\.execution\\.junit\\..*" |
| 110 | + }; |
| 111 | + |
| 112 | + // java.security.debug support |
| 113 | + private static final boolean DEBUG = AccessController.doPrivileged(new PrivilegedAction<Boolean>() { |
| 114 | + @Override |
| 115 | + public Boolean run() { |
| 116 | + try { |
| 117 | + String v = System.getProperty("java.security.debug"); |
| 118 | + // simple check that they are trying to debug |
| 119 | + return v != null && v.length() > 0; |
| 120 | + } catch (SecurityException e) { |
| 121 | + return false; |
| 122 | + } |
| 123 | + } |
| 124 | + }); |
| 125 | + |
| 126 | + @Override |
| 127 | + @SuppressForbidden(reason = "java.security.debug messages go to standard error") |
| 128 | + public void checkAccess(Thread t) { |
| 129 | + try { |
| 130 | + checkThreadAccess(t); |
| 131 | + } catch (SecurityException e) { |
| 132 | + if (DEBUG) { |
| 133 | + System.err.println("access: caller thread=" + Thread.currentThread()); |
| 134 | + System.err.println("access: target thread=" + t); |
| 135 | + debugThreadGroups(Thread.currentThread().getThreadGroup(), t.getThreadGroup()); |
| 136 | + } |
| 137 | + throw e; |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + @Override |
| 142 | + @SuppressForbidden(reason = "java.security.debug messages go to standard error") |
| 143 | + public void checkAccess(ThreadGroup g) { |
| 144 | + try { |
| 145 | + checkThreadGroupAccess(g); |
| 146 | + } catch (SecurityException e) { |
| 147 | + if (DEBUG) { |
| 148 | + System.err.println("access: caller thread=" + Thread.currentThread()); |
| 149 | + debugThreadGroups(Thread.currentThread().getThreadGroup(), g); |
| 150 | + } |
| 151 | + throw e; |
| 152 | + } |
| 153 | + } |
| 154 | + |
| 155 | + @SuppressForbidden(reason = "java.security.debug messages go to standard error") |
| 156 | + private void debugThreadGroups(final ThreadGroup caller, final ThreadGroup target) { |
| 157 | + System.err.println("access: caller group=" + caller); |
| 158 | + System.err.println("access: target group=" + target); |
| 159 | + } |
| 160 | + |
| 161 | + // thread permission logic |
| 162 | + |
| 163 | + private static final Permission MODIFY_THREAD_PERMISSION = new RuntimePermission("modifyThread"); |
| 164 | + private static final Permission MODIFY_ARBITRARY_THREAD_PERMISSION = new ThreadPermission("modifyArbitraryThread"); |
| 165 | + |
| 166 | + protected void checkThreadAccess(Thread t) { |
| 167 | + Objects.requireNonNull(t); |
| 168 | + |
| 169 | + // first, check if we can modify threads at all. |
| 170 | + checkPermission(MODIFY_THREAD_PERMISSION); |
| 171 | + |
| 172 | + // check the threadgroup, if its our thread group or an ancestor, its fine. |
| 173 | + final ThreadGroup source = Thread.currentThread().getThreadGroup(); |
| 174 | + final ThreadGroup target = t.getThreadGroup(); |
| 175 | + |
| 176 | + if (target == null) { |
| 177 | + return; // its a dead thread, do nothing. |
| 178 | + } else if (source.parentOf(target) == false) { |
| 179 | + checkPermission(MODIFY_ARBITRARY_THREAD_PERMISSION); |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + private static final Permission MODIFY_THREADGROUP_PERMISSION = new RuntimePermission("modifyThreadGroup"); |
| 184 | + private static final Permission MODIFY_ARBITRARY_THREADGROUP_PERMISSION = new ThreadPermission("modifyArbitraryThreadGroup"); |
| 185 | + |
| 186 | + protected void checkThreadGroupAccess(ThreadGroup g) { |
| 187 | + Objects.requireNonNull(g); |
| 188 | + |
| 189 | + // first, check if we can modify thread groups at all. |
| 190 | + checkPermission(MODIFY_THREADGROUP_PERMISSION); |
| 191 | + |
| 192 | + // check the threadgroup, if its our thread group or an ancestor, its fine. |
| 193 | + final ThreadGroup source = Thread.currentThread().getThreadGroup(); |
| 194 | + final ThreadGroup target = g; |
| 195 | + |
| 196 | + if (source == null) { |
| 197 | + return; // we are a dead thread, do nothing |
| 198 | + } else if (source.parentOf(target) == false) { |
| 199 | + checkPermission(MODIFY_ARBITRARY_THREADGROUP_PERMISSION); |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + // exit permission logic |
| 204 | + @Override |
| 205 | + public void checkExit(int status) { |
| 206 | + innerCheckExit(status); |
| 207 | + } |
| 208 | + |
| 209 | + /** |
| 210 | + * The "Uwe Schindler" algorithm. |
| 211 | + * |
| 212 | + * @param status the exit status |
| 213 | + */ |
| 214 | + protected void innerCheckExit(final int status) { |
| 215 | + AccessController.doPrivileged(new PrivilegedAction<Void>() { |
| 216 | + @Override |
| 217 | + public Void run() { |
| 218 | + final String systemClassName = System.class.getName(), |
| 219 | + runtimeClassName = Runtime.class.getName(); |
| 220 | + String exitMethodHit = null; |
| 221 | + for (final StackTraceElement se : Thread.currentThread().getStackTrace()) { |
| 222 | + final String className = se.getClassName(), methodName = se.getMethodName(); |
| 223 | + if ( |
| 224 | + ("exit".equals(methodName) || "halt".equals(methodName)) && |
| 225 | + (systemClassName.equals(className) || runtimeClassName.equals(className)) |
| 226 | + ) { |
| 227 | + exitMethodHit = className + '#' + methodName + '(' + status + ')'; |
| 228 | + continue; |
| 229 | + } |
| 230 | + |
| 231 | + if (exitMethodHit != null) { |
| 232 | + if (classesThatCanExit == null) { |
| 233 | + break; |
| 234 | + } |
| 235 | + if (classCanExit(className, classesThatCanExit)) { |
| 236 | + // this exit point is allowed, we return normally from closure: |
| 237 | + return null; |
| 238 | + } |
| 239 | + // anything else in stack trace is not allowed, break and throw SecurityException below: |
| 240 | + break; |
| 241 | + } |
| 242 | + } |
| 243 | + |
| 244 | + if (exitMethodHit == null) { |
| 245 | + // should never happen, only if JVM hides stack trace - replace by generic: |
| 246 | + exitMethodHit = "JVM exit method"; |
| 247 | + } |
| 248 | + throw new SecurityException(exitMethodHit + " calls are not allowed"); |
| 249 | + } |
| 250 | + }); |
| 251 | + |
| 252 | + // we passed the stack check, delegate to super, so default policy can still deny permission: |
| 253 | + super.checkExit(status); |
| 254 | + } |
| 255 | + |
| 256 | + static boolean classCanExit(final String className, final String[] classesThatCanExit) { |
| 257 | + for (final String classThatCanExit : classesThatCanExit) { |
| 258 | + if (className.matches(classThatCanExit)) { |
| 259 | + return true; |
| 260 | + } |
| 261 | + } |
| 262 | + return false; |
| 263 | + } |
| 264 | + |
| 265 | +} |
0 commit comments