Skip to content

Commit 1b3d529

Browse files
committed
Introduce secure security manager to project
This commit migrates SecureSM, our secure security manager implementation, from its own repository to being a sub-project of Elasticsearch.
2 parents 5e0be61 + e6bd34e commit 1b3d529

File tree

19 files changed

+1159
-215
lines changed

19 files changed

+1159
-215
lines changed

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ task verifyVersions {
145145
* after the backport of the backcompat code is complete.
146146
*/
147147
allprojects {
148-
ext.bwc_tests_enabled = false
148+
ext.bwc_tests_enabled = true
149149
}
150150

151151
task verifyBwcTestsEnabled {
@@ -185,6 +185,7 @@ subprojects {
185185
"org.elasticsearch:elasticsearch-cli:${version}": ':server:cli',
186186
"org.elasticsearch:elasticsearch-core:${version}": ':libs:elasticsearch-core',
187187
"org.elasticsearch:elasticsearch-nio:${version}": ':libs:elasticsearch-nio',
188+
"org.elasticsearch:elasticsearch-secure-sm:${version}": ':libs:secure-sm',
188189
"org.elasticsearch.client:elasticsearch-rest-client:${version}": ':client:rest',
189190
"org.elasticsearch.client:elasticsearch-rest-client-sniffer:${version}": ':client:sniffer',
190191
"org.elasticsearch.client:elasticsearch-rest-high-level-client:${version}": ':client:rest-high-level',

libs/secure-sm/build.gradle

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
import org.elasticsearch.gradle.precommit.PrecommitTasks
21+
22+
apply plugin: 'elasticsearch.build'
23+
apply plugin: 'nebula.maven-base-publish'
24+
apply plugin: 'nebula.maven-scm'
25+
26+
archivesBaseName = 'elasticsearch-secure-sm'
27+
28+
publishing {
29+
publications {
30+
nebula {
31+
artifactId = archivesBaseName
32+
}
33+
}
34+
}
35+
36+
dependencies {
37+
// do not add non-test compile dependencies to secure-sm without a good reason to do so
38+
39+
testCompile "com.carrotsearch.randomizedtesting:randomizedtesting-runner:${versions.randomizedrunner}"
40+
testCompile "junit:junit:${versions.junit}"
41+
testCompile "org.hamcrest:hamcrest-all:${versions.hamcrest}"
42+
43+
if (isEclipse == false || project.path == ":libs:secure-sm-tests") {
44+
testCompile("org.elasticsearch.test:framework:${version}") {
45+
exclude group: 'org.elasticsearch', module: 'secure-sm'
46+
}
47+
}
48+
}
49+
50+
forbiddenApisMain {
51+
signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')]
52+
}
53+
54+
if (isEclipse) {
55+
// in Eclipse the project is under a fake root so we need to change around the source sets
56+
sourceSets {
57+
if (project.path == ":libs:secure-sm") {
58+
main.java.srcDirs = ['java']
59+
main.resources.srcDirs = ['resources']
60+
} else {
61+
test.java.srcDirs = ['java']
62+
test.resources.srcDirs = ['resources']
63+
}
64+
}
65+
}
66+
67+
// JAR hell is part of core which we do not want to add as a dependency
68+
jarHell.enabled = false
69+
70+
namingConventions {
71+
testClass = 'junit.framework.TestCase'
72+
}
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
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+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.lang.annotation.ElementType;
23+
import java.lang.annotation.Retention;
24+
import java.lang.annotation.RetentionPolicy;
25+
import java.lang.annotation.Target;
26+
27+
/**
28+
* Annotation to suppress forbidden-apis errors inside a whole class, a method, or a field.
29+
*/
30+
@Retention(RetentionPolicy.CLASS)
31+
@Target({ ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
32+
@interface SuppressForbidden {
33+
String reason();
34+
}

0 commit comments

Comments
 (0)