/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.elasticsearch.script.groovy; import org.apache.lucene.util.Constants; import org.codehaus.groovy.control.MultipleCompilationErrorsException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.script.CompiledScript; import org.elasticsearch.script.ScriptException; import org.elasticsearch.script.ScriptService; import org.elasticsearch.test.ESTestCase; import groovy.lang.MissingPropertyException; import java.nio.file.Path; import java.security.PrivilegedActionException; import java.util.AbstractMap; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Tests for the Groovy security permissions */ public class GroovySecurityTests extends ESTestCase { private GroovyScriptEngineService se; @Override public void setUp() throws Exception { super.setUp(); se = new GroovyScriptEngineService(Settings.EMPTY); // otherwise will exit your VM and other bad stuff assumeTrue("test requires security manager to be enabled", System.getSecurityManager() != null); } @Override public void tearDown() throws Exception { se.close(); super.tearDown(); } public void testEvilGroovyScripts() throws Exception { // Plain test assertSuccess(""); // field access (via map) assertSuccess("def foo = doc['foo'].value; if (foo == null) { return 5; }"); // field access (via list) assertSuccess("def foo = mylist[0]; if (foo == null) { return 5; }"); // field access (via array) assertSuccess("def foo = myarray[0]; if (foo == null) { return 5; }"); // field access (via object) assertSuccess("def foo = myobject.primitive.toString(); if (foo == null) { return 5; }"); assertSuccess("def foo = myobject.object.toString(); if (foo == null) { return 5; }"); assertSuccess("def foo = myobject.list[0].primitive.toString(); if (foo == null) { return 5; }"); // List assertSuccess("def list = [doc['foo'].value, 3, 4]; def v = list.get(1); list.add(10)"); // Ranges assertSuccess("def range = 1..doc['foo'].value; def v = range.get(0)"); // Maps assertSuccess("def v = doc['foo'].value; def m = [:]; m.put(\"value\", v)"); // Times assertSuccess("def t = Instant.now().getMillis()"); // GroovyCollections assertSuccess("def n = [1,2,3]; GroovyCollections.max(n)"); // Groovy closures assertSuccess("[1, 2, 3, 4].findAll { it % 2 == 0 }"); assertSuccess("def buckets=[ [2, 4, 6, 8], [10, 12, 16, 14], [18, 22, 20, 24] ]; buckets[-3..-1].every { it.every { i -> i % 2 == 0 } }"); assertSuccess("def val = \"\"; [1, 2, 3, 4].each { val += it }; val"); // Groovy uses reflection to invoke closures. These reflective calls are optimized by the JVM after "sun.reflect.inflationThreshold" // invocations. After the inflation step, access to sun.reflect.MethodAccessorImpl is required from the security manager. This test, // assuming a inflation threshold below 100 (15 is current value on Oracle JVMs), checks that the relevant permission is available. assertSuccess("(1..100).collect{ it + 1 }"); // Fail cases: assertFailure("pr = Runtime.getRuntime().exec(\"touch /tmp/gotcha\"); pr.waitFor()", MissingPropertyException.class); // infamous: assertFailure("java.lang.Math.class.forName(\"java.lang.Runtime\")", PrivilegedActionException.class); // filtered directly by our classloader assertFailure("getClass().getClassLoader().loadClass(\"java.lang.Runtime\").availableProcessors()", PrivilegedActionException.class); // unfortunately, we have access to other classloaders (due to indy mechanism needing getClassLoader permission) // but we can't do much with them directly at least. assertFailure("myobject.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").availableProcessors()", SecurityException.class); assertFailure("d = new DateTime(); d.getClass().getDeclaredMethod(\"year\").setAccessible(true)", SecurityException.class); assertFailure("d = new DateTime(); d.\"${'get' + 'Class'}\"()." + "\"${'getDeclared' + 'Method'}\"(\"year\").\"${'set' + 'Accessible'}\"(false)", SecurityException.class); assertFailure("Class.forName(\"org.joda.time.DateTime\").getDeclaredMethod(\"year\").setAccessible(true)", MissingPropertyException.class); assertFailure("Eval.me('2 + 2')", MissingPropertyException.class); assertFailure("Eval.x(5, 'x + 2')", MissingPropertyException.class); assertFailure("d = new Date(); java.lang.reflect.Field f = Date.class.getDeclaredField(\"fastTime\");" + " f.setAccessible(true); f.get(\"fastTime\")", MultipleCompilationErrorsException.class); assertFailure("def methodName = 'ex'; Runtime.\"${'get' + 'Runtime'}\"().\"${methodName}ec\"(\"touch /tmp/gotcha2\")", MissingPropertyException.class); assertFailure("t = new Thread({ println 3 });", MultipleCompilationErrorsException.class); // test a directory we normally have access to, but the groovy script does not. Path dir = createTempDir(); // TODO: figure out the necessary escaping for windows paths here :) if (!Constants.WINDOWS) { assertFailure("new File(\"" + dir + "\").exists()", MultipleCompilationErrorsException.class); } } /** runs a script */ private void doTest(String script) { Map<String, Object> vars = new HashMap<String, Object>(); // we add a "mock document" containing a single field "foo" that returns 4 (abusing a jdk class with a getValue() method) vars.put("doc", Collections.singletonMap("foo", new AbstractMap.SimpleEntry<Object,Integer>(null, 4))); vars.put("mylist", Arrays.asList("foo")); vars.put("myarray", Arrays.asList("foo")); vars.put("myobject", new MyObject()); se.executable(new CompiledScript(ScriptService.ScriptType.INLINE, "test", "js", se.compile(script, Collections.<String, String>emptyMap())), vars).run(); } public static class MyObject { public int getPrimitive() { return 0; } public Object getObject() { return "value"; } public List<? extends Object> getList() { return Arrays.asList(new MyObject()); } } /** asserts that a script runs without exception */ private void assertSuccess(String script) { doTest(script); } /** asserts that a script triggers securityexception */ private void assertFailure(String script, Class<? extends Throwable> exceptionClass) { try { doTest(script); fail("did not get expected exception"); } catch (ScriptException expected) { Throwable cause = expected.getCause(); assertNotNull(cause); if (exceptionClass.isAssignableFrom(cause.getClass()) == false) { throw new AssertionError("unexpected exception: " + cause, expected); } } } }