delight.nashornsandbox.internal.JsSanitizer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of delight-nashorn-sandbox Show documentation
Show all versions of delight-nashorn-sandbox Show documentation
A safe sandbox to execute JavaScript code from Nashorn.
The newest version!
package delight.nashornsandbox.internal;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.ref.SoftReference;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import delight.nashornsandbox.SecuredJsCache;
import delight.nashornsandbox.exceptions.BracesException;
/**
* JavaScript sanitizer. Check for loops and inserts function call which breaks
* script execution when JS engine thread is interrupted.
*
*
* Created on 2017.11.22
*
*
* @author Marcin Golebski
* @version $Id$
*/
@SuppressWarnings("restriction")
public class JsSanitizer {
/** The resource name of inject.js script. */
private final static String INJECT_JS = "resources/inject.js";
private static final List inject_FUNCTIONS = Arrays.asList("exports.injectJs;", "window.injectJs;", "exports.injectJs;", "global.injectJs;");
/**
* The name of the JS function to be inserted into user script. To prevent
* collisions random suffix is added.
*/
final static String JS_INTERRUPTED_FUNCTION = "__if";
/**
* The name of the variable which holds reference to interruption checking
* class. To prevent collisions random suffix is added.
*/
final static String JS_INTERRUPTED_TEST = "__it";
/** Soft reference to the text of the js script. */
private static SoftReference injectScript = new SoftReference<>(null);
private final ScriptEngine scriptEngine;
/** JS beautify() function reference. */
private final Function jsInject;
private final SecuredJsCache securedJsCache;
JsSanitizer(final ScriptEngine scriptEngine, final int maxPreparedStatements) {
this.scriptEngine = scriptEngine;
this.securedJsCache = createSecuredJsCache(maxPreparedStatements);
assertScriptEngine();
Object beautifHandler = getInjectHandler(scriptEngine);
this.jsInject = injectAsFunction(beautifHandler);
}
JsSanitizer(final ScriptEngine scriptEngine, SecuredJsCache cache) {
this.scriptEngine = scriptEngine;
this.securedJsCache = cache;
assertScriptEngine();
Object injectHandler = getInjectHandler(scriptEngine);
this.jsInject = injectAsFunction(injectHandler);
}
private void assertScriptEngine() {
try {
scriptEngine.eval("var window = {};");
scriptEngine.eval("var exports = {};");
scriptEngine.eval("var global = {};");
// Object.assign polyfill
scriptEngine.eval("\"function\"!=typeof Object.assign&&Object.defineProperty(Object,\"assign\",{value:function(e,t){\"use strict\";if(null==e)throw new TypeError(\"Cannot convert undefined or null to object\");for(var n=Object(e),r=1;r linkedHashMap = new LinkedHashMap(maxPreparedStatements + 1, .75F, true) {
private static final long serialVersionUID = 1L;
// This method is called just after a new entry has been added
@Override
public boolean removeEldestEntry(final Map.Entry eldest) {
return size() > maxPreparedStatements;
}
};
return new LinkedHashMapSecuredJsCache(linkedHashMap);
}
private String getPreamble() {
final String clazzName = InterruptTest.class.getName();
final StringBuilder sb = new StringBuilder();
sb.append("var ").append(JS_INTERRUPTED_TEST).append("=Java.type('").append(clazzName).append("');");
sb.append("var ").append(JS_INTERRUPTED_FUNCTION).append("=function(){");
sb.append(JS_INTERRUPTED_TEST).append(".test();};\n");
return sb.toString();
}
private void checkJs(final String js) {
if (js.contains(JS_INTERRUPTED_FUNCTION) || js.contains(JS_INTERRUPTED_TEST)) {
throw new IllegalArgumentException(
"Script contains the illegal string [" + JS_INTERRUPTED_TEST + "," + JS_INTERRUPTED_FUNCTION + "]");
}
}
public String secureJs(final String js) throws ScriptException {
if (securedJsCache == null) {
return secureJsImpl(js);
}
ScriptException[] ex = new ScriptException[1];
String securedJs = securedJsCache.getOrCreate(js, ()->{
try {
return secureJsImpl(js);
} catch (BracesException e) {
ex[0] = e;
return null;
}
});
if (ex[0] != null) {
throw ex[0];
}
return securedJs;
}
private String secureJsImpl(final String js) throws BracesException {
checkJs(js);
final String injectedJs = injectInterruptionCalls(js);
// if no injection, no need to add preamble
if (!injectedJs.contains(JS_INTERRUPTED_FUNCTION)) {
return injectedJs;
} else {
final String preamble = getPreamble();
return preamble + injectedJs;
}
}
String injectInterruptionCalls(final String js) {
return jsInject.apply(js);
}
private static String getInjectJs() {
String script = injectScript.get();
if (script == null) {
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(
new BufferedInputStream(JsSanitizer.class.getResourceAsStream(INJECT_JS)), StandardCharsets.UTF_8))) {
final StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append('\n');
}
script = sb.toString();
} catch (final IOException e) {
throw new RuntimeException("Cannot find file: " + INJECT_JS, e);
}
injectScript = new SoftReference<>(script);
}
return script;
}
@SuppressWarnings("unchecked")
private static Function injectAsFunction(Object injectScript) {
if (NashornDetection.isStandaloneNashornScriptObjectMirror(injectScript)) {
return script -> {
org.openjdk.nashorn.api.scripting.ScriptObjectMirror scriptObjectMirror = (org.openjdk.nashorn.api.scripting.ScriptObjectMirror) injectScript;
return (String) scriptObjectMirror.call("injectJs", script, JS_INTERRUPTED_FUNCTION + "();");
};
}
if (injectScript instanceof Function, ?>) {
return script -> (String) ((Function