| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF 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.apache.cassandra.cql3.validation.entities; |
| |
| import java.security.AccessControlException; |
| import java.util.List; |
| |
| import org.junit.Assert; |
| import org.junit.Test; |
| |
| import org.apache.cassandra.config.Config; |
| import org.apache.cassandra.config.DatabaseDescriptor; |
| import org.apache.cassandra.cql3.CQLTester; |
| import org.apache.cassandra.cql3.functions.UDHelper; |
| import org.apache.cassandra.exceptions.FunctionExecutionException; |
| import org.apache.cassandra.service.ClientWarn; |
| |
| public class UFSecurityTest extends CQLTester |
| { |
| @Test |
| public void testSecurityPermissions() throws Throwable |
| { |
| createTable("CREATE TABLE %s (key int primary key, dval double)"); |
| execute("INSERT INTO %s (key, dval) VALUES (?, ?)", 1, 1d); |
| |
| // Java UDFs |
| |
| try |
| { |
| String fName = createFunction(KEYSPACE_PER_TEST, "double", |
| "CREATE OR REPLACE FUNCTION %s(val double) " + |
| "RETURNS NULL ON NULL INPUT " + |
| "RETURNS double " + |
| "LANGUAGE JAVA\n" + |
| "AS 'System.getProperty(\"foo.bar.baz\"); return 0d;';"); |
| execute("SELECT " + fName + "(dval) FROM %s WHERE key=1"); |
| Assert.fail(); |
| } |
| catch (FunctionExecutionException e) |
| { |
| assertAccessControlException("System.getProperty(\"foo.bar.baz\"); return 0d;", e); |
| } |
| |
| String[][] typesAndSources = |
| { |
| {"", "try { Class.forName(\"" + UDHelper.class.getName() + "\"); } catch (Exception e) { throw new RuntimeException(e); } return 0d;"}, |
| {"sun.misc.Unsafe", "sun.misc.Unsafe.getUnsafe(); return 0d;"}, |
| {"", "try { Class.forName(\"sun.misc.Unsafe\"); } catch (Exception e) { throw new RuntimeException(e); } return 0d;"}, |
| {"java.nio.file.FileSystems", "try {" + |
| " java.nio.file.FileSystems.getDefault(); return 0d;" + |
| "} catch (Exception t) {" + |
| " throw new RuntimeException(t);" + |
| '}'}, |
| {"java.nio.channels.FileChannel", "try {" + |
| " java.nio.channels.FileChannel.open(java.nio.file.FileSystems.getDefault().getPath(\"/etc/passwd\")).close(); return 0d;" + |
| "} catch (Exception t) {" + |
| " throw new RuntimeException(t);" + |
| '}'}, |
| {"java.nio.channels.SocketChannel", "try {" + |
| " java.nio.channels.SocketChannel.open().close(); return 0d;" + |
| "} catch (Exception t) {" + |
| " throw new RuntimeException(t);" + |
| '}'}, |
| {"java.io.FileInputStream", "try {" + |
| " new java.io.FileInputStream(\"./foobar\").close(); return 0d;" + |
| "} catch (Exception t) {" + |
| " throw new RuntimeException(t);" + |
| '}'}, |
| {"java.lang.Runtime", "try {" + |
| " java.lang.Runtime.getRuntime(); return 0d;" + |
| "} catch (Exception t) {" + |
| " throw new RuntimeException(t);" + |
| '}'}, |
| {"org.apache.cassandra.service.StorageService", |
| "try {" + |
| " org.apache.cassandra.service.StorageService v = org.apache.cassandra.service.StorageService.instance; v.isShutdown(); return 0d;" + |
| "} catch (Exception t) {" + |
| " throw new RuntimeException(t);" + |
| '}'}, |
| {"java.net.ServerSocket", "try {" + |
| " new java.net.ServerSocket().bind(); return 0d;" + |
| "} catch (Exception t) {" + |
| " throw new RuntimeException(t);" + |
| '}'}, |
| {"java.io.FileOutputStream","try {" + |
| " new java.io.FileOutputStream(\".foo\"); return 0d;" + |
| "} catch (Exception t) {" + |
| " throw new RuntimeException(t);" + |
| '}'}, |
| {"java.lang.Runtime", "try {" + |
| " java.lang.Runtime.getRuntime().exec(\"/tmp/foo\"); return 0d;" + |
| "} catch (Exception t) {" + |
| " throw new RuntimeException(t);" + |
| '}'} |
| }; |
| |
| for (String[] typeAndSource : typesAndSources) |
| { |
| assertInvalidMessage(typeAndSource[0] + " cannot be resolved", |
| "CREATE OR REPLACE FUNCTION " + KEYSPACE + ".invalid_class_access(val double) " + |
| "RETURNS NULL ON NULL INPUT " + |
| "RETURNS double " + |
| "LANGUAGE JAVA\n" + |
| "AS '" + typeAndSource[1] + "';"); |
| } |
| |
| // JavaScript UDFs |
| |
| try |
| { |
| String fName = createFunction(KEYSPACE_PER_TEST, "double", |
| "CREATE OR REPLACE FUNCTION %s(val double) " + |
| "RETURNS NULL ON NULL INPUT " + |
| "RETURNS double " + |
| "LANGUAGE javascript\n" + |
| "AS 'org.apache.cassandra.service.StorageService.instance.isShutdown(); 0;';"); |
| execute("SELECT " + fName + "(dval) FROM %s WHERE key=1"); |
| Assert.fail("Javascript security check failed"); |
| } |
| catch (FunctionExecutionException e) |
| { |
| assertAccessControlException("", e); |
| } |
| |
| String[] javascript = |
| { |
| "java.lang.management.ManagmentFactory.getThreadMXBean(); 0;", |
| "new java.io.FileInputStream(\"/tmp/foo\"); 0;", |
| "new java.io.FileOutputStream(\"/tmp/foo\"); 0;", |
| "java.nio.file.FileSystems.getDefault().createFileExclusively(\"./foo_bar_baz\"); 0;", |
| "java.nio.channels.FileChannel.open(java.nio.file.FileSystems.getDefault().getPath(\"/etc/passwd\")); 0;", |
| "java.nio.channels.SocketChannel.open(); 0;", |
| "new java.net.ServerSocket().bind(null); 0;", |
| "var thread = new java.lang.Thread(); thread.start(); 0;", |
| "java.lang.System.getProperty(\"foo.bar.baz\"); 0;", |
| "java.lang.Runtime.getRuntime().exec(\"/tmp/foo\"); 0;", |
| "java.lang.Runtime.getRuntime().loadLibrary(\"foobar\"); 0;", |
| "java.lang.Runtime.getRuntime().loadLibrary(\"foobar\"); 0;", |
| // TODO these (ugly) calls are still possible - these can consume CPU (as one could do with an evil loop, too) |
| // "java.lang.Runtime.getRuntime().traceMethodCalls(true); 0;", |
| // "java.lang.Runtime.getRuntime().gc(); 0;", |
| // "java.lang.Runtime.getRuntime(); 0;", |
| }; |
| |
| for (String script : javascript) |
| { |
| try |
| { |
| String fName = createFunction(KEYSPACE_PER_TEST, "double", |
| "CREATE OR REPLACE FUNCTION %s(val double) " + |
| "RETURNS NULL ON NULL INPUT " + |
| "RETURNS double " + |
| "LANGUAGE javascript\n" + |
| "AS '" + script + "';"); |
| execute("SELECT " + fName + "(dval) FROM %s WHERE key=1"); |
| Assert.fail("Javascript security check failed: " + script); |
| } |
| catch (FunctionExecutionException e) |
| { |
| assertAccessControlException(script, e); |
| } |
| } |
| |
| String script = "java.lang.Class.forName(\"java.lang.System\"); 0;"; |
| String fName = createFunction(KEYSPACE_PER_TEST, "double", |
| "CREATE OR REPLACE FUNCTION %s(val double) " + |
| "RETURNS NULL ON NULL INPUT " + |
| "RETURNS double " + |
| "LANGUAGE javascript\n" + |
| "AS '" + script + "';"); |
| assertInvalidThrowMessage("Java reflection not supported when class filter is present", |
| FunctionExecutionException.class, |
| "SELECT " + fName + "(dval) FROM %s WHERE key=1"); |
| } |
| |
| private static void assertAccessControlException(String script, FunctionExecutionException e) |
| { |
| for (Throwable t = e; t != null && t != t.getCause(); t = t.getCause()) |
| if (t instanceof AccessControlException) |
| return; |
| Assert.fail("no AccessControlException for " + script + " (got " + e + ')'); |
| } |
| |
| @Test |
| public void testAmokUDF() throws Throwable |
| { |
| createTable("CREATE TABLE %s (key int primary key, dval double)"); |
| execute("INSERT INTO %s (key, dval) VALUES (?, ?)", 1, 1d); |
| |
| long udfWarnTimeout = DatabaseDescriptor.getUserDefinedFunctionWarnTimeout(); |
| long udfFailTimeout = DatabaseDescriptor.getUserDefinedFunctionFailTimeout(); |
| int maxTries = 5; |
| for (int i = 1; i <= maxTries; i++) |
| { |
| try |
| { |
| // short timeout |
| DatabaseDescriptor.setUserDefinedFunctionWarnTimeout(10); |
| DatabaseDescriptor.setUserDefinedFunctionFailTimeout(250); |
| // don't kill the unit test... - default policy is "die" |
| DatabaseDescriptor.setUserFunctionTimeoutPolicy(Config.UserFunctionTimeoutPolicy.ignore); |
| |
| ClientWarn.instance.captureWarnings(); |
| String fName = createFunction(KEYSPACE_PER_TEST, "double", |
| "CREATE OR REPLACE FUNCTION %s(val double) " + |
| "RETURNS NULL ON NULL INPUT " + |
| "RETURNS double " + |
| "LANGUAGE JAVA\n" + |
| "AS 'long t=System.currentTimeMillis()+110; while (t>System.currentTimeMillis()) { }; return 0d;'"); |
| execute("SELECT " + fName + "(dval) FROM %s WHERE key=1"); |
| List<String> warnings = ClientWarn.instance.getWarnings(); |
| Assert.assertNotNull(warnings); |
| Assert.assertFalse(warnings.isEmpty()); |
| ClientWarn.instance.resetWarnings(); |
| |
| // Java UDF |
| |
| fName = createFunction(KEYSPACE_PER_TEST, "double", |
| "CREATE OR REPLACE FUNCTION %s(val double) " + |
| "RETURNS NULL ON NULL INPUT " + |
| "RETURNS double " + |
| "LANGUAGE JAVA\n" + |
| "AS 'long t=System.currentTimeMillis()+500; while (t>System.currentTimeMillis()) { }; return 0d;';"); |
| assertInvalidMessage("ran longer than 250ms", "SELECT " + fName + "(dval) FROM %s WHERE key=1"); |
| |
| // Javascript UDF |
| |
| fName = createFunction(KEYSPACE_PER_TEST, "double", |
| "CREATE OR REPLACE FUNCTION %s(val double) " + |
| "RETURNS NULL ON NULL INPUT " + |
| "RETURNS double " + |
| "LANGUAGE JAVASCRIPT\n" + |
| "AS 'var t=java.lang.System.currentTimeMillis()+500; while (t>java.lang.System.currentTimeMillis()) { }; 0;';"); |
| assertInvalidMessage("ran longer than 250ms", "SELECT " + fName + "(dval) FROM %s WHERE key=1"); |
| |
| return; |
| } |
| catch (Error | RuntimeException e) |
| { |
| if (i == maxTries) |
| throw e; |
| } |
| finally |
| { |
| // reset to defaults |
| DatabaseDescriptor.setUserDefinedFunctionWarnTimeout(udfWarnTimeout); |
| DatabaseDescriptor.setUserDefinedFunctionFailTimeout(udfFailTimeout); |
| } |
| } |
| } |
| |
| } |