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
Original file line number Diff line number Diff line change
Expand Up @@ -19,72 +19,61 @@

package org.apache.ranger.plugin.util;

import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static java.util.Objects.requireNonNull;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.function.Predicate;

public class GraalScriptEngineCreator implements ScriptEngineCreator {
private static final Logger LOG = LoggerFactory.getLogger(GraalScriptEngineCreator.class);

private static final String ENGINE_NAME = "graal.js";
private static final String CONFIG_PREDIX_JVM = "polyglot";
private static final String CONFIG_PREDIX_PLUGIN = "ranger.plugin.script.";
private static final String CONFIG_JAVA_CLASS_PATH = "java.class.path";

// instance variables
private final Map<String, Boolean> graalVmConfigs = new HashMap<>();
private static final String ENGINE_NAME = "graal.js";
private static final String CLS_HOST_ACCESS = "org.graalvm.polyglot.HostAccess";
private static final String CLS_HOST_ACCESS_BUILDER = "org.graalvm.polyglot.HostAccess$Builder";
private static final String CLS_CONTEXT = "org.graalvm.polyglot.Context";
private static final String CLS_CONTEXT_BUILDER = "org.graalvm.polyglot.Context$Builder";
private static final String CLS_ENGINE = "org.graalvm.polyglot.Engine";
private static final String CLS_GRAAL_JS_ENGINE = "com.oracle.truffle.js.scriptengine.GraalJSScriptEngine";

// Script-API classes whose public instance methods are exported to scripts.
// getDeclaredMethods() returns only methods declared directly in each class,
// NOT inherited ones (e.g. Object.getClass()) — so the reflection chain is
// closed without needing @HostAccess.Export annotations on those classes.
private static final String[] SCRIPT_API_CLASSES = new String[] {
"org.apache.ranger.plugin.policyengine.RangerRequestScriptEvaluator",
"org.apache.ranger.plugin.contextenricher.RangerTagForEval"
};
private final Method createMethod;
private final Object ctxBuilder;

// default constructor
public GraalScriptEngineCreator() {
Map<String, Boolean> graalVmConfigsDefault = new HashMap<>(4); //setting smallest size, which is big enough to avoid expand
Configuration configuration = new Configuration();
FilenameFilter fileNameFilter = (dir, name) -> name.startsWith("ranger-") && name.endsWith("security.xml");

graalVmConfigsDefault.put("polyglot.js.allowHostAccess", Boolean.TRUE); //default is true for backward(Nashorn) compatibility
graalVmConfigsDefault.put("polyglot.js.nashorn-compat", Boolean.TRUE); //default is true for backward(Nashorn) compatibility

for (String file : findFiles(fileNameFilter)) {
configuration.addResource(new Path(file));
Method createMethod = null;
Object builder = null;
try {
Object hostAccess = buildHostAccess();
builder = buildContextBuilder(hostAccess);
Class<?> engineCls = Class.forName(CLS_ENGINE);
Class<?> graalJsCls = Class.forName(CLS_GRAAL_JS_ENGINE);
Class<?> ctxBldCls = Class.forName(CLS_CONTEXT_BUILDER);
createMethod = graalJsCls.getMethod("create", engineCls, ctxBldCls);
} catch (Throwable t) {
LOG.warn("GraalScriptEngineCreator(): failed to initialize", t);
} finally {
this.createMethod = createMethod;
this.ctxBuilder = builder;
}

graalVmConfigs.putAll(getGraalVmConfigs(configuration, graalVmConfigsDefault));
}

public ScriptEngine getScriptEngine(ClassLoader clsLoader) {
ScriptEngine ret = null;

if (clsLoader == null) {
clsLoader = getDefaultClassLoader();
}

try {
ScriptEngineManager mgr = new ScriptEngineManager(clsLoader);

ret = mgr.getEngineByName(ENGINE_NAME);

if (ret != null) {
// enable configured script features
Bindings bindings = ret.getBindings(ScriptContext.ENGINE_SCOPE);
bindings.putAll(graalVmConfigs);
ret.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
if (createMethod != null && ctxBuilder != null) {
ret = (ScriptEngine) createMethod.invoke(null, null, ctxBuilder);
}
} catch (Throwable t) {
LOG.debug("GraalScriptEngineCreator.getScriptEngine(): failed to create engine type {}", ENGINE_NAME, t);
Expand All @@ -96,68 +85,37 @@ public ScriptEngine getScriptEngine(ClassLoader clsLoader) {
return ret;
}

private Map<String, Boolean> getGraalVmConfigs(Configuration configuration, Map<String, Boolean> graalVmConfigsDefault) {
LOG.debug("===>> GraalScriptEngineCreator.getGraalVmConfigs()");

Map<String, Boolean> ret = new HashMap<>();

// set configs from ranger security config values, if present
for (Map.Entry<String, String> entry : configuration.getPropsWithPrefix(CONFIG_PREDIX_PLUGIN).entrySet()) {
String key = entry.getKey();
String value = entry.getValue();

if (StringUtils.isNotBlank(value)) {
ret.put(key, Boolean.valueOf(value));
}
}

// add JVM options if not already set
for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
if (entry.getKey().toString().startsWith(CONFIG_PREDIX_JVM)) {
String key = entry.getKey().toString();
String value = entry.getValue().toString();

if (StringUtils.isNotBlank(value) && ret.get(key) == null) {
ret.put(key, Boolean.valueOf(value));
private Object buildHostAccess() throws Exception {
Class<?> hostAccessCls = Class.forName(CLS_HOST_ACCESS);
Class<?> haBuilderCls = Class.forName(CLS_HOST_ACCESS_BUILDER);
Object haBuilder = hostAccessCls.getMethod("newBuilder").invoke(null);
Method allowAccessMethod = haBuilderCls.getMethod("allowAccess", Executable.class);
for (String className : SCRIPT_API_CLASSES) {
try {
for (Method m : Class.forName(className).getDeclaredMethods()) {
if (Modifier.isPublic(m.getModifiers()) && !Modifier.isStatic(m.getModifiers())) {
allowAccessMethod.invoke(haBuilder, m);
}
}
} catch (ClassNotFoundException e) {
LOG.warn("GraalScriptEngineCreator.buildHostAccess(): class not found: {}", className);
}
}

// add default values if not already set
for (Map.Entry<String, Boolean> entry : graalVmConfigsDefault.entrySet()) {
String key = entry.getKey();
Boolean value = entry.getValue();

ret.putIfAbsent(key, value);
}

LOG.debug("<<=== GraalScriptEngineCreator.getGraalVmConfigs(): ret={}", ret);

return ret;
return haBuilderCls.getMethod("build").invoke(haBuilder);
}

private Set<String> findFiles(FilenameFilter filenameFilter) {
String classPath = System.getProperty(CONFIG_JAVA_CLASS_PATH);
List<String> configDirs = new ArrayList<>(5);
Set<String> ret = new HashSet<>();

for (String path : classPath.split(":")) {
if (!path.endsWith("jar")) { //ignore jars
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
configDirs.add(path);
}
}

for (String configDir : configDirs) {
File confDir = new File(configDir);
if (confDir.isDirectory()) {
for (File file : requireNonNull(confDir.listFiles(filenameFilter))) {
ret.add(file.getAbsolutePath());
}
}
}
return ret;
private Object buildContextBuilder(Object hostAccess) throws Exception {
Class<?> hostAccessCls = Class.forName(CLS_HOST_ACCESS);
Class<?> contextCls = Class.forName(CLS_CONTEXT);
Class<?> ctxBuilderCls = Class.forName(CLS_CONTEXT_BUILDER);
Object builder = contextCls.getMethod("newBuilder", String[].class).invoke(null, new Object[] {new String[] {"js"}});
builder = ctxBuilderCls.getMethod("allowExperimentalOptions", boolean.class).invoke(builder, true);
builder = ctxBuilderCls.getMethod("allowAllAccess", boolean.class).invoke(builder, false);
builder = ctxBuilderCls.getMethod("allowHostAccess", hostAccessCls).invoke(builder, hostAccess);
builder = ctxBuilderCls.getMethod("allowHostClassLookup", Predicate.class).invoke(builder, (Predicate<String>) s -> false);
builder = ctxBuilderCls.getMethod("allowHostClassLoading", boolean.class).invoke(builder, false);
builder = ctxBuilderCls.getMethod("allowCreateThread", boolean.class).invoke(builder, false);
builder = ctxBuilderCls.getMethod("allowNativeAccess", boolean.class).invoke(builder, false);
return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@
import javax.script.ScriptEngineManager;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -555,6 +557,64 @@ public void testGetAllTagTypes() {
Assertions.assertEquals(new HashSet<>(Arrays.asList("PCI", "PII")), evaluator.evaluateScript("ctx.getAllTagTypes()"));
}

@Test
void testBlockJavaClassReferencesWithDefaultConfig() {
RangerRequestScriptEvaluator evaluator = createEvaluator();
long nonce = System.nanoTime();

String mJavaType = tempFilePath("javatype", nonce);
String mPackages = tempFilePath("packages", nonce);
String mCtxRef = tempFilePath("ctxref", nonce);
String mRetRef = tempFilePath("retref", nonce);
String mTagRef = tempFilePath("tagref", nonce);
String mTagAttr = tempFilePath("tagattr", nonce);
String mNewFile = tempFilePath("newfile", nonce);

Function<String, String> reflectExec = marker ->
"var Str = OBJ.getClass().getClassLoader().loadClass('java.lang.String');"
+ "var rtClz = OBJ.getClass().getClassLoader().loadClass('java.lang.Runtime');"
+ "var rt = rtClz.getMethod('getRuntime').invoke(null);"
+ "rtClz.getMethod('exec', Str).invoke(rt, 'touch " + marker + "').waitFor();";

Map<String, String[]> vectors = new HashMap<>();
vectors.put("Java.type('java.lang.Runtime').exec()", new String[] {
"var p = Java.type('java.lang.Runtime').getRuntime().exec('touch " + mJavaType + "'); p.waitFor();", mJavaType});
vectors.put("java.lang.Runtime (Packages namespace)", new String[] {
"var p = java.lang.Runtime.getRuntime().exec('touch " + mPackages + "'); p.waitFor();", mPackages});
vectors.put("reflection off ctx", new String[] {
reflectExec.apply(mCtxRef).replace("OBJ", "ctx"), mCtxRef});
vectors.put("reflection off returned ctx.getCurrentTag()", new String[] {
reflectExec.apply(mRetRef).replace("OBJ", "ctx.getCurrentTag()"), mRetRef});
vectors.put("reflection off bound 'tag'", new String[] {
reflectExec.apply(mTagRef).replace("OBJ", "tag"), mTagRef});
vectors.put("reflection off bound 'tagAttr'", new String[] {
reflectExec.apply(mTagAttr).replace("OBJ", "tagAttr"), mTagAttr});
vectors.put("constructor new java.io.File().createNewFile()", new String[] {
"var f = new java.io.File('" + mNewFile + "'); f.createNewFile();", mNewFile});

List<String> escaped = new ArrayList<>();
for (Map.Entry<String, String[]> e : vectors.entrySet()) {
File m = new File(e.getValue()[1]);
m.delete(); // Just to make sure file does not exist before the script runs. This is a clean-state guarantee
evaluator.evaluateScript(e.getValue()[0]); // returns null when blocked; never rethrows
if (m.exists()) { // The file existing after eval is the ground truth that the OS command actually ran
escaped.add(e.getKey() + " -> created " + m);
m.delete();
}
}
Assertions.assertTrue(escaped.isEmpty(), "Sandbox escape OS command executed via:\n " + String.join("\n ", escaped));
}

private RangerRequestScriptEvaluator createEvaluator() {
RangerAccessRequest request = createRequest("test-user", Collections.emptySet(), Collections.emptySet(),
Collections.singletonList(new RangerTag("PII", Collections.singletonMap("attr1", "v1"))));
return new RangerRequestScriptEvaluator(request, scriptEngine, false);
}

private static String tempFilePath(String tag, long nonce) {
return new File(System.getProperty("java.io.tmpdir"), "ranger_test_" + tag + "_" + nonce).getAbsolutePath();
}

RangerAccessRequest createRequest(String userName, Set<String> userGroups, Set<String> userRoles, List<RangerTag> resourceTags) {
RangerAccessResource resource = mock(RangerAccessResource.class);

Expand Down