Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/main/java/com/google/escapevelocity/MethodFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static java.util.stream.Collectors.toSet;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Table;
import java.lang.reflect.Method;
Expand All @@ -39,6 +40,40 @@
*/
class MethodFinder {

/**
* Methods that are blocked from being invoked in templates to prevent reflection-based attacks
* such as Remote Code Execution via {@code getClass().forName(...).getMethod(...).invoke(...)}.
*
* <p>The key is the fully-qualified class name. If the value is an empty set, <em>all</em> methods
* on that class are blocked. Otherwise only the named methods are blocked.
*/
private static final ImmutableMap<String, ImmutableSet<String>> BLOCKED_METHODS =
ImmutableMap.<String, ImmutableSet<String>>builder()
// Prevent obtaining Class objects from arbitrary instances.
.put("java.lang.Object", ImmutableSet.of("getClass"))
// Prevent reflective class loading, method/constructor/field lookup, and instantiation.
.put("java.lang.Class", ImmutableSet.of(
"forName", "newInstance",
"getMethod", "getMethods", "getDeclaredMethod", "getDeclaredMethods",
"getConstructor", "getConstructors",
"getDeclaredConstructor", "getDeclaredConstructors",
"getField", "getFields", "getDeclaredField", "getDeclaredFields",
"getClassLoader"))
// Prevent reflective invocation and instantiation.
.put("java.lang.reflect.Method", ImmutableSet.of("invoke"))
.put("java.lang.reflect.Constructor", ImmutableSet.of("newInstance"))
.put("java.lang.reflect.Field", ImmutableSet.of("get", "set"))
// Prevent direct command execution.
.put("java.lang.Runtime", ImmutableSet.of("exec", "getRuntime"))
.put("java.lang.ProcessBuilder", ImmutableSet.of())
// Prevent class loading.
.put("java.lang.ClassLoader", ImmutableSet.of())
.put("java.lang.Thread", ImmutableSet.of(
"getContextClassLoader", "setContextClassLoader"))
// Prevent JVM shutdown and environment access.
.put("java.lang.System", ImmutableSet.of("exit", "setSecurityManager"))
.build();

/**
* For a given class and name, returns all public methods of that name in the class, as previously
* determined by {@link #publicMethodsWithName}. The set of methods for a given class and name is
Expand Down Expand Up @@ -76,6 +111,7 @@ private ImmutableSet<Method> uncachedPublicMethodsWithName(Class<?> startClass,
Set<Method> methods =
Arrays.stream(startClass.getMethods())
.filter(m -> m.getName().equals(name))
.filter(m -> !isMethodBlocked(m))
.collect(toSet());
if (!classIsPublic(startClass)) {
methods =
Expand All @@ -89,6 +125,35 @@ private ImmutableSet<Method> uncachedPublicMethodsWithName(Class<?> startClass,
return ImmutableSet.copyOf(methods);
}

/**
* Returns true if the given method is on the blocklist. A method is blocked if the class that
* declares it (or any of its ancestors) appears in {@link #BLOCKED_METHODS} with either an empty
* set (meaning all methods are blocked) or a set containing the method name.
*/
private static boolean isMethodBlocked(Method method) {
Class<?> declaringClass = method.getDeclaringClass();
return isBlockedInHierarchy(declaringClass, method.getName());
}

private static boolean isBlockedInHierarchy(Class<?> clazz, String methodName) {
if (clazz == null) {
return false;
}
ImmutableSet<String> blocked = BLOCKED_METHODS.get(clazz.getName());
if (blocked != null && (blocked.isEmpty() || blocked.contains(methodName))) {
return true;
}
if (isBlockedInHierarchy(clazz.getSuperclass(), methodName)) {
return true;
}
for (Class<?> iface : clazz.getInterfaces()) {
if (isBlockedInHierarchy(iface, methodName)) {
return true;
}
}
return false;
}

private static final String THIS_PACKAGE = getPackageName(Node.class) + ".";

/**
Expand Down
102 changes: 102 additions & 0 deletions src/test/java/com/google/escapevelocity/TemplateTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1525,6 +1525,108 @@ public void evaluate() {
expectException("#evaluate(23)", "Argument to #evaluate must be a string: 23");
}

@Test
public void evaluateBlocksReflectionChainViaGetClass() throws IOException {
String template = "#evaluate($payload)";
String payload =
"#set($clazz = $dummy.getClass())"
+ "$clazz.forName('java.lang.Runtime')";
Map<String, Object> vars = new HashMap<>();
vars.put("dummy", "anything");
vars.put("payload", payload);
Template parsed = Template.parseFrom(new StringReader(template));
EvaluationException e =
assertThrows(EvaluationException.class, () -> parsed.evaluate(vars));
assertThat(e).hasMessageThat().contains("no method getClass");
}

@Test
public void blocksGetClassOnAnyObject() throws IOException {
Template parsed = Template.parseFrom(new StringReader("$x.getClass()"));
EvaluationException e =
assertThrows(
EvaluationException.class,
() -> parsed.evaluate(ImmutableMap.of("x", "hello")));
assertThat(e).hasMessageThat().contains("no method getClass");
}

@Test
public void blocksClassForName() throws IOException {
Template parsed = Template.parseFrom(new StringReader("$C.forName('java.lang.Runtime')"));
EvaluationException e =
assertThrows(
EvaluationException.class,
() -> parsed.evaluate(ImmutableMap.of("C", Class.class)));
assertThat(e).hasMessageThat().contains("no method forName");
}

@Test
public void blocksClassGetMethod() throws IOException {
Template parsed = Template.parseFrom(new StringReader("$C.getMethod('getRuntime', $empty)"));
Map<String, Object> vars = new HashMap<>();
vars.put("C", Runtime.class);
vars.put("empty", new Class<?>[0]);
EvaluationException e =
assertThrows(EvaluationException.class, () -> parsed.evaluate(vars));
assertThat(e).hasMessageThat().contains("no method getMethod");
}

@Test
public void blocksMethodInvoke() throws Exception {
java.lang.reflect.Method method = String.class.getMethod("valueOf", int.class);
Template parsed = Template.parseFrom(new StringReader("$m.invoke(null, 42)"));
Map<String, Object> vars = new HashMap<>();
vars.put("m", method);
EvaluationException e =
assertThrows(EvaluationException.class, () -> parsed.evaluate(vars));
assertThat(e).hasMessageThat().contains("no method invoke");
}

@Test
public void blocksRuntimeExec() throws IOException {
Template parsed = Template.parseFrom(new StringReader("$rt.exec('id')"));
EvaluationException e =
assertThrows(
EvaluationException.class,
() -> parsed.evaluate(ImmutableMap.of("rt", Runtime.getRuntime())));
assertThat(e).hasMessageThat().contains("no method exec");
}

@Test
public void blocksProcessBuilder() throws IOException {
Template parsed = Template.parseFrom(new StringReader("$pb.start()"));
EvaluationException e =
assertThrows(
EvaluationException.class,
() ->
parsed.evaluate(
ImmutableMap.of("pb", new ProcessBuilder("echo", "pwned"))));
assertThat(e).hasMessageThat().contains("no method start");
}

@Test
public void blocksFullRceChainViaEvaluate() throws IOException {
String rcePayload =
"#set($m=$Class.forName('java.lang.Runtime').getMethod('getRuntime', $noParamTypes))"
+ "#set($rt=$m.invoke(null, $noArgs))"
+ "$rt.exec('touch /tmp/test-pwned')";
Map<String, Object> vars = new HashMap<>();
vars.put("Class", Class.class);
vars.put("noParamTypes", new Class<?>[0]);
vars.put("noArgs", new Object[0]);
vars.put("payload", rcePayload);
Template parsed = Template.parseFrom(new StringReader("#evaluate($payload)"));
assertThrows(EvaluationException.class, () -> parsed.evaluate(vars));
assertThat(new java.io.File("/tmp/test-pwned").exists()).isFalse();
}

@Test
public void allowsSafeMethodsOnClass() throws IOException {
Template parsed = Template.parseFrom(new StringReader("$Integer.getName()"));
String result = parsed.evaluate(ImmutableMap.of("Integer", Integer.class));
assertThat(result).isEqualTo("java.lang.Integer");
}

@Test
public void nullReference() throws IOException {
Map<String, Object> vars = Collections.singletonMap("foo", null);
Expand Down