Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiment standalone nashorn #111

Merged
merged 5 commits into from
Mar 14, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 8 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@
<artifactId>js-beautify</artifactId>
<version>1.9.0</version>
</dependency>

<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
<version>15.2</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package delight.nashornsandbox.internal;

import jdk.nashorn.api.scripting.ClassFilter;

public class JdkNashornClassFilter extends SandboxClassFilter implements ClassFilter {

@Override
public boolean exposeToScripts(final String className) {
return super.exposeToScripts(className);
}

}
67 changes: 55 additions & 12 deletions src/main/java/delight/nashornsandbox/internal/JsSanitizer.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

import delight.nashornsandbox.SecuredJsCache;
import delight.nashornsandbox.exceptions.BracesException;
import jdk.nashorn.api.scripting.ScriptObjectMirror;

/**
* JavaScript sanitizer. Check for loops and inserts function call which breaks
Expand All @@ -36,6 +35,28 @@
*/
@SuppressWarnings("restriction")
public class JsSanitizer {

private static final Class<?> JDK_NASHORN_ScriptObjectMirror_CLASS;
private static final Class<?> STANDALONE_NASHORN_ScriptObjectMirror_CLASS;

static {
Class<?> tmp_JDK_NASHORN_ScriptObjectMirror_CLASS = null;
// TODO what behavior do we want here?
try {
tmp_JDK_NASHORN_ScriptObjectMirror_CLASS = Class.forName("jdk.nashorn.api.scripting.ScriptObjectMirror");
} catch (ClassNotFoundException e) {
System.out.println("JDK Nashorn not found");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume in the final version we wouldn't want to log out here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, like I wrote in the description, this is a basis for discussion and must not be merged as is.
I think there should be somewhere a log statement indicating which version of Nashorn is used and why, but that should probably be at debug level.

}
JDK_NASHORN_ScriptObjectMirror_CLASS= tmp_JDK_NASHORN_ScriptObjectMirror_CLASS;
Class<?> tmp_STANDALONE_NASHORN_ScriptObjectMirror_CLASS = null;
try {
tmp_STANDALONE_NASHORN_ScriptObjectMirror_CLASS = Class.forName("org.openjdk.nashorn.api.scripting.ScriptObjectMirror");
} catch (ClassNotFoundException e) {
System.out.println("Standalone Nashorn not found");
}
STANDALONE_NASHORN_ScriptObjectMirror_CLASS = tmp_STANDALONE_NASHORN_ScriptObjectMirror_CLASS;
}

private static class PoisonPil {
Pattern pattern;
String replacement;
Expand Down Expand Up @@ -103,7 +124,7 @@ private static class PoisonPil {
private final ScriptEngine scriptEngine;

/** JS beautify() function reference. */
private final Object jsBeautify;
private final Function<String, String> jsBeautify;

private final SecuredJsCache securedJsCache;

Expand All @@ -115,15 +136,17 @@ private static class PoisonPil {
this.allowNoBraces = allowBraces;
this.securedJsCache = createSecuredJsCache(maxPreparedStatements);
assertScriptEngine();
this.jsBeautify = getBeautifHandler(scriptEngine);
Object beautifHandler = getBeautifHandler(scriptEngine);
this.jsBeautify = beautifierAsFunction(beautifHandler);
}

JsSanitizer(final ScriptEngine scriptEngine, final boolean allowBraces, SecuredJsCache cache) {
this.scriptEngine = scriptEngine;
this.allowNoBraces = allowBraces;
this.securedJsCache = cache;
assertScriptEngine();
this.jsBeautify = getBeautifHandler(scriptEngine);
Object beautifHandler = getBeautifHandler(scriptEngine);
this.jsBeautify = beautifierAsFunction(beautifHandler);
}

private void assertScriptEngine() {
Expand Down Expand Up @@ -181,7 +204,7 @@ public boolean removeEldestEntry(final Map.Entry<String, String> eldest) {
Pattern.compile("for [^\\{]+$"),
Pattern.compile("^\\s*do [^\\{]*$", Pattern.MULTILINE),
Pattern.compile("^[^\\}]*while [^\\{]+$", Pattern.MULTILINE));

/**
* After beautifier every braces should be in place, if not, or too many we need
* to prevent script execution.
Expand All @@ -195,12 +218,12 @@ void checkBraces(final String beautifiedJs) throws BracesException {
if (allowNoBraces) {
return;
}

String withoutComments = RemoveComments.perform(beautifiedJs);
for (final Pattern pattern : LACK_EXPECTED_BRACES) {
final Matcher matcher = pattern.matcher(withoutComments);
if (matcher.find()) {

String line = "";
int index = matcher.start();

Expand All @@ -214,7 +237,7 @@ void checkBraces(final String beautifiedJs) throws BracesException {
int commentParaCount = line.length() - line.replace("//", "").length();
if (singleParaCount % 2 != 0 || doubleParaCount % 2 != 0 || commentParaCount > 0) {
// for in string

} else {
throw new BracesException("Potentially no block braces after function|for|while|do. Found ["+matcher.group()+"]. "+
"To disable this exception, please set the option `allowNoBraces(true)`");
Expand Down Expand Up @@ -286,11 +309,8 @@ private String secureJsImpl(final String js) throws BracesException {
}
}

@SuppressWarnings("unchecked")
String beautifyJs(final String js) {
if (jsBeautify instanceof ScriptObjectMirror) return (String) ((ScriptObjectMirror) jsBeautify).call("beautify", js, BEAUTIFY_OPTIONS);
else if (jsBeautify instanceof Function<?, ?>) return (String) ((Function<Object[], Object>) jsBeautify).apply(new Object[] { js, BEAUTIFY_OPTIONS });
else throw new RuntimeException("Unsupported handler type for jsBeautify: " + jsBeautify.getClass().getName());
return jsBeautify.apply(js);
}

private static String getBeautifyJs() {
Expand All @@ -312,4 +332,27 @@ private static String getBeautifyJs() {
return script;
}


@SuppressWarnings("unchecked")
private static Function<String, String> beautifierAsFunction(Object beautifyScript) {
if (JDK_NASHORN_ScriptObjectMirror_CLASS != null && JDK_NASHORN_ScriptObjectMirror_CLASS.isInstance(beautifyScript)) {
return script -> {
jdk.nashorn.api.scripting.ScriptObjectMirror scriptObjectMirror = (jdk.nashorn.api.scripting.ScriptObjectMirror) beautifyScript;
return (String) scriptObjectMirror.call("beautify", script, BEAUTIFY_OPTIONS);
};
}

if (STANDALONE_NASHORN_ScriptObjectMirror_CLASS != null && STANDALONE_NASHORN_ScriptObjectMirror_CLASS.isInstance(beautifyScript)) {
return script -> {
org.openjdk.nashorn.api.scripting.ScriptObjectMirror scriptObjectMirror = (org.openjdk.nashorn.api.scripting.ScriptObjectMirror) beautifyScript;
return (String) scriptObjectMirror.call("beautify", script, BEAUTIFY_OPTIONS);
};
}

if (beautifyScript instanceof Function<?, ?>) {
return script -> (String) ((Function<Object[], Object>) beautifyScript).apply(new Object[]{script, BEAUTIFY_OPTIONS});
}

throw new RuntimeException("Unsupported handler type for jsBeautify: " + beautifyScript.getClass().getName());
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package delight.nashornsandbox.internal;

import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.script.Bindings;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.script.*;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -19,7 +16,6 @@
import delight.nashornsandbox.SecuredJsCache;
import delight.nashornsandbox.exceptions.ScriptCPUAbuseException;
import delight.nashornsandbox.exceptions.ScriptMemoryAbuseException;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;

/**
* Nashorn sandbox implementation.
Expand All @@ -39,9 +35,40 @@
@SuppressWarnings("restriction")
public class NashornSandboxImpl implements NashornSandbox {

private static final Class<?> JDK_NASHORN_NashornScriptEngineFactory_CLASS;
private static final Class<?> JDK_NASHORN_ClassFilter_CLASS;
private static final Class<?> STANDALONE_NASHORN_NashornScriptEngineFactory_CLASS;
private static final Class<?> STANDALONE_NASHORN_ClassFilter_CLASS;

static {
Class<?> tmp_JDK_NASHORN_NashornScriptEngineFactory_CLASS = null;
Class<?> tmp_JDK_NASHORN_ClassFilter_CLASS = null;
// TODO what behavior do we want here?
// TODO move all JDK/standalone code to some dedicated class?
try {
tmp_JDK_NASHORN_NashornScriptEngineFactory_CLASS = Class.forName("jdk.nashorn.api.scripting.NashornScriptEngineFactory");
tmp_JDK_NASHORN_ClassFilter_CLASS = Class.forName("jdk.nashorn.api.scripting.ClassFilter");
} catch (ClassNotFoundException e) {
System.out.println("JDK Nashorn not found");
}
JDK_NASHORN_NashornScriptEngineFactory_CLASS = tmp_JDK_NASHORN_NashornScriptEngineFactory_CLASS;
JDK_NASHORN_ClassFilter_CLASS = tmp_JDK_NASHORN_ClassFilter_CLASS;

Class<?> tmp_STANDALONE_NASHORN_NashornScriptEngineFactory_CLASS = null;
Class<?> tmp_STANDALONE_NASHORN_ClassFilter_CLASS = null;
try {
tmp_STANDALONE_NASHORN_NashornScriptEngineFactory_CLASS = Class.forName("org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory");
tmp_STANDALONE_NASHORN_ClassFilter_CLASS = Class.forName("org.openjdk.nashorn.api.scripting.ClassFilter");
} catch (ClassNotFoundException e) {
System.out.println("Standalone Nashorn not found");
}
STANDALONE_NASHORN_NashornScriptEngineFactory_CLASS = tmp_STANDALONE_NASHORN_NashornScriptEngineFactory_CLASS;
STANDALONE_NASHORN_ClassFilter_CLASS = tmp_STANDALONE_NASHORN_ClassFilter_CLASS;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably better to manage this in a centralised place?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most certainly, that should move to a class like NashornDetection, that would do the detection and debug logs about what is going on.

static final Logger LOG = LoggerFactory.getLogger(NashornSandbox.class);

protected final SandboxClassFilter sandboxClassFilter = new SandboxClassFilter();
protected final SandboxClassFilter sandboxClassFilter;

protected final ScriptEngine scriptEngine;

Expand Down Expand Up @@ -95,15 +122,48 @@ public NashornSandboxImpl(ScriptEngine engine, String... params) {
"The engine parameter --no-java is not supported. Using it would interfere with the injected code to test for infinite loops.");
}
}
sandboxClassFilter = createSandboxClassFilter();
this.scriptEngine = engine == null
? new NashornScriptEngineFactory().getScriptEngine(params, this.getClass().getClassLoader(),
this.sandboxClassFilter)
? createNashornScriptEngineFactory(params)
: engine;
this.maxPreparedStatements = 0;
this.allow(InterruptTest.class);
this.engineAsserted = new AtomicBoolean(false);
}

private SandboxClassFilter createSandboxClassFilter() {
if (JDK_NASHORN_ClassFilter_CLASS != null) {
return new JdkNashornClassFilter();
}
if (STANDALONE_NASHORN_ClassFilter_CLASS != null) {
return new StandaloneNashornClassFilter();
}
throw new IllegalStateException("Neither jdk.nashorn.api.scripting.ClassFilter or org.openjdk.nashorn.api.scripting.ClassFilter is present");
}

public ScriptEngine createNashornScriptEngineFactory(String ... params) {
try {
Object nashornScriptEngineFactory = null;
Class<?> classFilterClass = null;
if (JDK_NASHORN_NashornScriptEngineFactory_CLASS != null) {
nashornScriptEngineFactory = JDK_NASHORN_NashornScriptEngineFactory_CLASS.getConstructor().newInstance();
classFilterClass = JDK_NASHORN_ClassFilter_CLASS;
} else if (STANDALONE_NASHORN_NashornScriptEngineFactory_CLASS != null) {
nashornScriptEngineFactory = STANDALONE_NASHORN_NashornScriptEngineFactory_CLASS.getConstructor().newInstance();
classFilterClass = STANDALONE_NASHORN_ClassFilter_CLASS;
}
if (nashornScriptEngineFactory == null) {
throw new IllegalStateException("Neither jdk.nashorn.api.scripting.NashornScriptEngineFactory or org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory is present");
}

Method getScriptEngine = nashornScriptEngineFactory.getClass().getDeclaredMethod("getScriptEngine", String[].class, ClassLoader.class, classFilterClass);
return (ScriptEngine) getScriptEngine.invoke(nashornScriptEngineFactory, params, this.getClass().getClassLoader(),
classFilterClass.cast(this.sandboxClassFilter));
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}

private synchronized void assertScriptEngine() {
try {
if (!engineAsserted.get()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import java.util.HashSet;
import java.util.Set;

import jdk.nashorn.api.scripting.ClassFilter;

/**
* The class Filter.
*
Expand All @@ -17,11 +15,9 @@
* @version $Id$
*/
@SuppressWarnings("restriction")
public class SandboxClassFilter implements ClassFilter {
public abstract class SandboxClassFilter {
private final Set<Class<?>> allowed;
private final Set<String> stringCache;

@Override
public boolean exposeToScripts(final String className) {
return stringCache.contains(className);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package delight.nashornsandbox.internal;

import org.openjdk.nashorn.api.scripting.ClassFilter;

public class StandaloneNashornClassFilter extends SandboxClassFilter implements ClassFilter {

@Override
public boolean exposeToScripts(final String className) {
return super.exposeToScripts(className);
}

}
53 changes: 40 additions & 13 deletions src/test/java/delight/nashornsandbox/TestAccessFunction.java
Original file line number Diff line number Diff line change
@@ -1,29 +1,56 @@
package delight.nashornsandbox;

import java.util.function.Function;

import javax.script.ScriptException;

import delight.nashornsandbox.exceptions.ScriptCPUAbuseException;
import org.junit.Assert;
import org.junit.Test;

import delight.nashornsandbox.NashornSandbox;
import delight.nashornsandbox.NashornSandboxes;
import delight.nashornsandbox.exceptions.ScriptCPUAbuseException;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import javax.script.ScriptException;
import java.lang.reflect.InvocationTargetException;

@SuppressWarnings("all")
public class TestAccessFunction {

@Test
public void test_access_variable() throws ScriptCPUAbuseException, ScriptException {
private static final Class<?> JDK_NASHORN_ScriptObjectMirror_CLASS;
private static final Class<?> STANDALONE_NASHORN_ScriptObjectMirror_CLASS;

static {
Class<?> tmp_JDK_NASHORN_ScriptObjectMirror_CLASS = null;
// TODO what behavior do we want here?
try {
tmp_JDK_NASHORN_ScriptObjectMirror_CLASS = Class.forName("jdk.nashorn.api.scripting.ScriptObjectMirror");
} catch (ClassNotFoundException e) {
System.out.println("JDK Nashorn not found");
}
JDK_NASHORN_ScriptObjectMirror_CLASS= tmp_JDK_NASHORN_ScriptObjectMirror_CLASS;
Class<?> tmp_STANDALONE_NASHORN_ScriptObjectMirror_CLASS = null;
try {
tmp_STANDALONE_NASHORN_ScriptObjectMirror_CLASS = Class.forName("org.openjdk.nashorn.api.scripting.ScriptObjectMirror");
} catch (ClassNotFoundException e) {
System.out.println("Standalone Nashorn not found");
}
STANDALONE_NASHORN_ScriptObjectMirror_CLASS = tmp_STANDALONE_NASHORN_ScriptObjectMirror_CLASS;
}

@Test
public void test_access_variable() throws ScriptCPUAbuseException, ScriptException, InvocationTargetException, IllegalAccessException {
final NashornSandbox sandbox = NashornSandboxes.create();
sandbox.eval("function callMe() { return 42; };");
final Object _get = sandbox.get("callMe");
Assert.assertEquals(Integer.valueOf(42), ((ScriptObjectMirror) _get).call(this));
Assert.assertEquals(Integer.valueOf(42), findAndCall(_get));
final Object _eval = sandbox.eval("callMe");
Assert.assertEquals(Integer.valueOf(42), ((ScriptObjectMirror) _eval).call(this));
Assert.assertEquals(Integer.valueOf(42), findAndCall(_get));
}


private Object findAndCall(Object _get) {
if (JDK_NASHORN_ScriptObjectMirror_CLASS != null && JDK_NASHORN_ScriptObjectMirror_CLASS.isInstance(_get)) {
jdk.nashorn.api.scripting.ScriptObjectMirror scriptObjectMirror = (jdk.nashorn.api.scripting.ScriptObjectMirror) _get;
return scriptObjectMirror.call(_get);
}

if (STANDALONE_NASHORN_ScriptObjectMirror_CLASS != null && STANDALONE_NASHORN_ScriptObjectMirror_CLASS.isInstance(_get)) {
org.openjdk.nashorn.api.scripting.ScriptObjectMirror scriptObjectMirror = (org.openjdk.nashorn.api.scripting.ScriptObjectMirror) _get;
return scriptObjectMirror.call(_get);
}
throw new IllegalStateException("Neither JDK nor standalone Nashorn has been found");
}
}