From a7774f2d8b9b1bef3ae3c04bceae0be23ad8999c Mon Sep 17 00:00:00 2001 From: Robert Muir Date: Tue, 5 May 2015 00:33:29 -0400 Subject: [PATCH] Run groovy scripts with no permissions --- .../org/elasticsearch/bootstrap/ESPolicy.java | 8 +- .../script/GroovySecurityTests.java | 126 ++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/elasticsearch/script/GroovySecurityTests.java diff --git a/src/main/java/org/elasticsearch/bootstrap/ESPolicy.java b/src/main/java/org/elasticsearch/bootstrap/ESPolicy.java index befef74251b..e0d8dd873bf 100644 --- a/src/main/java/org/elasticsearch/bootstrap/ESPolicy.java +++ b/src/main/java/org/elasticsearch/bootstrap/ESPolicy.java @@ -19,6 +19,8 @@ package org.elasticsearch.bootstrap; +import org.elasticsearch.common.SuppressForbidden; + import java.net.URI; import java.security.Permission; import java.security.PermissionCollection; @@ -41,8 +43,12 @@ public class ESPolicy extends Policy { this.dynamic = dynamic; } - @Override + @Override @SuppressForbidden(reason = "I know what i am doing") public boolean implies(ProtectionDomain domain, Permission permission) { + // run groovy scripts with no permissions + if ("/groovy/script".equals(domain.getCodeSource().getLocation().getFile())) { + return false; + } return template.implies(domain, permission) || dynamic.implies(permission); } } diff --git a/src/test/java/org/elasticsearch/script/GroovySecurityTests.java b/src/test/java/org/elasticsearch/script/GroovySecurityTests.java new file mode 100644 index 00000000000..23b78ac9b6b --- /dev/null +++ b/src/test/java/org/elasticsearch/script/GroovySecurityTests.java @@ -0,0 +1,126 @@ +/* + * 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; + +import org.apache.lucene.util.Constants; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.search.ShardSearchFailure; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ElasticsearchIntegrationTest; +import org.junit.Test; + +import java.nio.file.Path; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures; +import static org.hamcrest.CoreMatchers.equalTo; + +/** + * Tests for the Groovy security permissions + */ +@ElasticsearchIntegrationTest.ClusterScope(scope = ElasticsearchIntegrationTest.Scope.TEST, numDataNodes = 0) +public class GroovySecurityTests extends ElasticsearchIntegrationTest { + + @Override + public void setUp() throws Exception { + super.setUp(); + assumeTrue("security manager is enabled", System.getSecurityManager() != null); + } + + @Test + public void testEvilGroovyScripts() throws Exception { + int nodes = randomIntBetween(1, 3); + Settings nodeSettings = ImmutableSettings.builder() + .put("script.inline", true) + .put("script.indexed", true) + .build(); + internalCluster().startNodesAsync(nodes, nodeSettings).get(); + client().admin().cluster().prepareHealth().setWaitForNodes(nodes + "").get(); + + client().prepareIndex("test", "doc", "1").setSource("foo", 5).setRefresh(true).get(); + + // Plain test + assertSuccess(""); + // 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)"); + + // Fail cases: + // AccessControlException[access denied ("java.io.FilePermission" "<>" "execute")] + assertFailure("pr = Runtime.getRuntime().exec(\\\"touch /tmp/gotcha\\\"); pr.waitFor()"); + + // AccessControlException[access denied ("java.lang.RuntimePermission" "accessClassInPackage.sun.reflect")] + assertFailure("d = new DateTime(); d.getClass().getDeclaredMethod(\\\"year\\\").setAccessible(true)"); + assertFailure("d = new DateTime(); d.\\\"${'get' + 'Class'}\\\"()." + + "\\\"${'getDeclared' + 'Method'}\\\"(\\\"year\\\").\\\"${'set' + 'Accessible'}\\\"(false)"); + assertFailure("Class.forName(\\\"org.joda.time.DateTime\\\").getDeclaredMethod(\\\"year\\\").setAccessible(true)"); + + // AccessControlException[access denied ("groovy.security.GroovyCodeSourcePermission" "/groovy/shell")] + assertFailure("Eval.me('2 + 2')"); + assertFailure("Eval.x(5, 'x + 2')"); + + // AccessControlException[access denied ("java.lang.RuntimePermission" "accessDeclaredMembers")] + assertFailure("d = new Date(); java.lang.reflect.Field f = Date.class.getDeclaredField(\\\"fastTime\\\");" + + " f.setAccessible(true); f.get(\\\"fastTime\\\")"); + + // AccessControlException[access denied ("java.io.FilePermission" "<>" "execute")] + assertFailure("def methodName = 'ex'; Runtime.\\\"${'get' + 'Runtime'}\\\"().\\\"${methodName}ec\\\"(\\\"touch /tmp/gotcha2\\\")"); + + // 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) { + // access denied ("java.io.FilePermission" ".../tempDir-00N" "read") + assertFailure("new File(\\\"" + dir + "\\\").exists()"); + } + } + + private void assertSuccess(String script) { + logger.info("--> script: " + script); + SearchResponse resp = client().prepareSearch("test") + .setSource("{\"query\": {\"match_all\": {}}," + + "\"sort\":{\"_script\": {\"script\": \""+ script + + "; doc['foo'].value + 2\", \"type\": \"number\", \"lang\": \"groovy\"}}}").get(); + assertNoFailures(resp); + assertEquals(1, resp.getHits().getTotalHits()); + assertThat(resp.getHits().getAt(0).getSortValues(), equalTo(new Object[]{7.0})); + } + + private void assertFailure(String script) { + logger.info("--> script: " + script); + SearchResponse resp = client().prepareSearch("test") + .setSource("{\"query\": {\"match_all\": {}}," + + "\"sort\":{\"_script\": {\"script\": \""+ script + + "; doc['foo'].value + 2\", \"type\": \"number\", \"lang\": \"groovy\"}}}").get(); + assertEquals(0, resp.getHits().getTotalHits()); + ShardSearchFailure fails[] = resp.getShardFailures(); + // TODO: GroovyScriptExecutionException needs work + for (ShardSearchFailure fail : fails) { + assertTrue(fail.getCause().toString().contains("AccessControlException[access denied")); + } + } +}