Skip to content

Instantly share code, notes, and snippets.

@seraphy
Created May 23, 2014 12:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seraphy/8cdcf11bf2da40fb908d to your computer and use it in GitHub Desktop.
Save seraphy/8cdcf11bf2da40fb908d to your computer and use it in GitHub Desktop.
Java8のNashornをサンドボックスで評価する。カスタムポリシークラスもしくはポリシーファイルによるポリシーの設定方法と、undocumentedなNashornのオプションである --no-java の利用によるJavaコードの制限方法についての例。
<?xml version="1.0" encoding="UTF-8"?>
<project name="project" default="default">
<description>SandboxScriptTest</description>
<target name="default" description="description">
<mkdir dir="work"/>
<javac
srcdir="src"
destdir="work"
target="1.8"
debug="on"
includeantruntime="false">
</javac>
<copy todir="work">
<fileset dir="src">
<exclude name="**/*.java"/>
</fileset>
</copy>
<jar basedir="work" destfile="SandboxScriptTest.jar">
<manifest>
<attribute name="Main-Class" value="jp.seraphyware.sandboxscript.Main"/>
</manifest>
</jar>
<delete dir="work"/>
</target>
</project>
package jp.seraphyware.sandboxscript;
import java.beans.Expression;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
public class Main {
/**
* Nashornスクリプトエンジンを取得する.
* @param noJava スクリプト内でのJavaへの直接アクセスを禁止する場合はtrue
* @return Nashornのスクリプトエンジン
*/
private static ScriptEngine getNashornScriptEngine(ScriptEngineManager manager, boolean noJava) {
// Nashornのスクリプトエンジンを検索する
ScriptEngine nashorn = null;
for (ScriptEngineFactory factory : manager.getEngineFactories()) {
if (factory.getNames().contains("nashorn")) {
ArrayList<String> options = new ArrayList<>();
if (noJava) {
// --no-javaオプションにより、JavaScript内でのJava.type()や、
// java, javaxなどのトップレベルパッケージへのアクセスを禁ずることで
// javaへの直接アクセスを不可とする.
// (スクリプトにJava側から渡されたJavaオブジェクトの利用は可能)
// https://www.mail-archive.com/nashorn-dev@openjdk.java.net/msg01152.html
// http://comments.gmane.org/gmane.comp.java.openjdk.nashorn.devel/2001
options.add("--no-java");
}
Expression exp = new Expression(
factory,
"getScriptEngine",
new Object[] {
options.toArray(new String[options.size()])
});
try {
nashorn = (ScriptEngine) exp.getValue();
break;
} catch (Exception ex) {
System.err.println(ex);
// 他のnashorn対応のエンジンがあるかもしれないので継続する.
}
}
}
if (nashorn == null) {
throw new RuntimeException("nashornのScriptEngineを取得できません。");
}
return nashorn;
}
/**
* エントリポイント
* @param args
* @throws Exception
*/
public static void main(String... args) throws Exception {
// セキュリティマネージャを有効にする
if (Arrays.stream(args).anyMatch(arg -> arg.equals("--policyFile"))) {
System.out.println("ポリシーファイルを使用します");
SandboxUtils.enableSecurityManagerByPolicyFile();
} else if (Arrays.stream(args).noneMatch(arg -> arg.equals("--no-security"))) {
System.out.println("カスタムポリシークラスを使用します");
SandboxUtils.enableSecutiryManager();
} else {
System.out.println("セキュリティチェックを行いません");
}
// Nashornスクリプトエンジンを取得する.
boolean noJava = Arrays.stream(args).anyMatch(arg -> arg.equals("--no-java"));
System.out.println("nashorn: --no-java=" + noJava);
// スクリプトマネージャからスクリプトエンジンを取得する
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine nashorn = getNashornScriptEngine(manager, noJava);
// ScriptEngine nashorn = manager.getEngineByName("nashorn"); // 通常の方法
// NashornにJava側から変数・関数を定義する
nashorn.put("console", (ScriptLog) System.out::println);
nashorn.put("clog", (Consumer<Object>) System.err::println);
// Nashornにローカルファイルへの書き込みを許可する関数を定義する
ArrayList<Writer> autoclosePool = new ArrayList<>();
nashorn.put("create_file",
(Function<String, Writer>) name -> createLocalFile(name, autoclosePool));
// スクリプトを読み込む
try (Reader rd = Files.newBufferedReader(Paths.get("testscript.js"))) {
// 作成したアクセスコントロールコンテキストを明示して
// セキュリティチェックを掛ける.
try {
SandboxUtils.doInSandbox(() -> {
// スクリプトを評価する
nashorn.eval(rd);
// スクリプト結果を評価する
Invocable invocable = (Invocable) nashorn;
// メソッドを呼び出す(#1)
invocable.invokeFunction("myMethod1");
// メソッドを呼び出す(#2)
ArrayList<String> inpArgs = new ArrayList<String>(
Arrays.asList(new String[] {"aaa", "bbb", "ccc"}));
List<?> retMyMethod2 = (List<?>) invocable
.invokeFunction("myMethod2", inpArgs);
retMyMethod2.forEach(System.out::println);
// オブジェクトをインターフェイスに見立てる(#1)
Runnable act = invocable.getInterface(
nashorn.get("myAction"), Runnable.class);
act.run();
// オブジェクトをインターフェイスに見立てる(#2)
MyFunc myFunc = invocable.getInterface(
nashorn.get("myFunc"), MyFunc.class);
String ret1 = myFunc.myMethod1("foo");
String ret2 = myFunc.myMethod2("bar");
System.out.println("myFunc ret=" + ret1 + ret2);
return null;
});
} catch (Exception se) {
System.out.println("キャッチされなかった例外: " + se);
}
} finally {
// スクリプト内で閉じ忘れたものを閉じる
for (Writer wr : autoclosePool) {
wr.close();
}
}
}
/**
* スクリプト用のログインターフェイス
*/
@FunctionalInterface
public interface ScriptLog {
void log(Object msg);
}
/**
* ローカルファイルへの書き込みを許可しWriterオブジェクトを返す.<br>
* @param name ローカルファイル名
* @return ライター
*/
private static Writer createLocalFile(String name, List<Writer> autoclosePool) {
return AccessController.doPrivileged(
(PrivilegedAction<Writer>) () -> {
try {
Path path = Paths.get(name).normalize();
if (!path.isAbsolute() && path.getNameCount() == 1) {
BufferedWriter wr = Files.newBufferedWriter(path);
if (autoclosePool != null) {
autoclosePool.add(wr);
}
return new PrintWriter(wr);
}
throw new SecurityException("許可されないパスです: " + path);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
});
}
// オブジェクトを取得するためのインターフェイス
// ※ ジェネリックにするとうまく取れない
// ※ デフォルトメソッドを含むと使えなくなる (java8u5にて)
// https://bugs.openjdk.java.net/browse/JDK-8031359
public interface MyFunc {
String myMethod1(String msg1);
String myMethod2(String msg2);
// default void show(String msg) {
// System.out.println(myMethod(msg));
// }
}
}
package jp.seraphyware.sandboxscript;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.AllPermission;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.Policy;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class SandboxUtils {
private SandboxUtils() {
super();
}
/**
* セキュリティマネージャを有効にする.<br>
* jreと、jre/lib/ext、および、このクラスが所属する
* プロテクションドメインを全権とし、それ以外はサンドボックスとする.
*/
public static void enableSecutiryManager() {
// jre/lib/ext上のjarファイルのURLを取得する.
Set<URI> extUrls = new HashSet<>();
String pathSeparator = System.getProperty("path.separator");
String extDirs = System.getProperty("java.ext.dirs");
for (String extDir : extDirs.split(pathSeparator)) {
Path extDirPath = Paths.get(extDir);
if (Files.isDirectory(extDirPath)) {
try (DirectoryStream<Path> stream =
Files.newDirectoryStream(extDirPath)) {
for (Path file : stream) {
if (Files.isRegularFile(file)) {
extUrls.add(file.toUri());
}
}
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
// 現在のプロテクションドメインと、
// 加えてjre/lib/ext上のjarに対して全権を付与するポリシー
Policy policy = new Policy() {
// 全権
private Permissions allperms = new Permissions();
{
allperms.add(new AllPermission());
}
// 一度解決したProtectionDomainのキャッシュ
private Map<ProtectionDomain, PermissionCollection> pdmap =
new ConcurrentHashMap<>();
@Override
public PermissionCollection getPermissions(ProtectionDomain domain) {
if (domain != null) {
PermissionCollection pdperm = pdmap.get(domain);
if (pdperm == null) {
pdperm = pdmap.computeIfAbsent(domain, this::getPermission);
}
return pdperm;
}
return new Permissions();
}
/**
* 指定されたプロテクションドメインに対するパーミッションコレクションを返す.<br>
* @param domain ドメイン
* @return パーミッションコレクション、権限がなければ空のパーミッション
*/
private PermissionCollection getPermission(ProtectionDomain domain) {
URL url = domain.getCodeSource().getLocation();
try {
if (url != null && extUrls.contains(url.toURI())) {
return allperms;
}
} catch (URISyntaxException ex) {
// 無視する
}
return new Permissions();
}
};
// ポリシーとセキュリティマネージャを有効とする
Policy.setPolicy(policy);
System.setSecurityManager(new SecurityManager());
}
/**
* アプリケーション用のポリシーファイルに従いセキュリティポリシーを構成し、
* セキュリティマネージャを有効とする.<br>
*/
public static void enableSecurityManagerByPolicyFile() {
// リソースからポリシーファイルの取得
URL policyURL = SandboxUtils.class.getResource("security.policy");
if (policyURL == null) {
throw new SecurityException("ポリシーファイルがみつかりません");
}
// このクラスのあるプロテクションドメインのコードソースを取得する.
// ポリシーファイル中の"app.codebase"変数で展開するもの.
ProtectionDomain pd = SandboxUtils.class.getProtectionDomain();
CodeSource cs = pd.getCodeSource();
URL loc = cs.getLocation();
// ポリシーファイルのURLをシステムプロパティに設定する
// 先頭にイコールがある場合はデフォルトのセキュリティポリシーを無視する.
// http://docs.oracle.com/javase/7/docs/technotes/guides/security/PolicyFiles.html
System.setProperty("java.security.policy", "=" + policyURL.toExternalForm());
// システムプロパティにセットする
System.setProperty("app.codebase", loc.toExternalForm());
// セキュリティマネージャを有効にする.
// 明示的にポリシーファイルが指定されているので、
// アプリ用のポリシーファイルが読み込まれる.
System.setSecurityManager(new SecurityManager());
}
/**
* サンドボックスで実行する
* @param job ジョブ
* @return 結果
* @throws PrivilegedActionException ジョブ中の例外
*/
public static <T> T doInSandbox(
PrivilegedExceptionAction<T> job
) throws PrivilegedActionException {
// 空の権限コレクションを作成する = サンドボックス状態
Permissions perms = new Permissions();
// サンドボックス状態となるプロテクションドメインを定義する
ProtectionDomain domain = new ProtectionDomain(new CodeSource(null,
(Certificate[]) null), perms);
// サンドボックス状態のプロテクションドメインでの
// 権限チェックを行うためのアクセスコントロールコンテキストを作成する
AccessControlContext accessCtx = new AccessControlContext(
new ProtectionDomain[] { domain });
return AccessController.doPrivileged(job, accessCtx);
}
}
// jre/lib/ext上のライブラリ群
// java8のnashornも、ここにある.
grant codeBase "file:${{java.ext.dirs}}/*" {
permission java.security.AllPermission;
};
// アプリケーション自身
grant codeBase "${app.codebase}" {
permission java.security.AllPermission;
};
// それ以外
grant {
permission java.lang.RuntimePermission "stopThread";
permission java.net.SocketPermission "localhost:0", "listen";
permission java.util.PropertyPermission "java.version", "read";
permission java.util.PropertyPermission "java.vendor", "read";
permission java.util.PropertyPermission "java.vendor.url", "read";
permission java.util.PropertyPermission "java.class.version", "read";
permission java.util.PropertyPermission "os.name", "read";
permission java.util.PropertyPermission "os.version", "read";
permission java.util.PropertyPermission "os.arch", "read";
permission java.util.PropertyPermission "file.separator", "read";
permission java.util.PropertyPermission "path.separator", "read";
permission java.util.PropertyPermission "line.separator", "read";
permission java.util.PropertyPermission "java.specification.version", "read";
permission java.util.PropertyPermission "java.specification.vendor", "read";
permission java.util.PropertyPermission "java.specification.name", "read";
permission java.util.PropertyPermission "java.vm.specification.version", "read";
permission java.util.PropertyPermission "java.vm.specification.vendor", "read";
permission java.util.PropertyPermission "java.vm.specification.name", "read";
permission java.util.PropertyPermission "java.vm.version", "read";
permission java.util.PropertyPermission "java.vm.vendor", "read";
permission java.util.PropertyPermission "java.vm.name", "read";
};
// エンジンに設定されたグローバル関数の呼び出し
clog("javascript started.");
// エンジンに設定されたグローバル変数の呼び出し
console.log(new Date());
// 許可されたファイルへの書き込み
clog('許可されたファイルへの書き込みテスト');
var fh = create_file('hello_world.txt');
fh.println(new Date());
for (var idx = 0; idx < 100; idx++) {
fh.println('idx=' + idx);
}
fh.close();
//Javaのクラスローダへのアクセス
try {
var cls = fh.getClass();
clog('クラス: ' + cls);
var clsldr = cls.getClassLoader();
clog('クラスローダ: ' + clsldr);
} catch (ex) {
// セキュリティマネージャが有効であればNashornのスクリプトから
// クラスローダへのアクセスは禁止される
// access denied ("java.lang.RuntimePermission" "nashorn.JavaReflection")
clog('error!! ' + ex);
}
//Javaの直接利用
clog('スクリプト内からのファイルへの書き込みテスト');
try {
var FileWriter = Java.type('java.io.FileWriter');
var fw = new FileWriter('c:/temp/dummy.txt');
fw.close();
} catch (ex) {
// サンドボックス内で評価する場合には、
// セキュリティ例外が発生する.
// また、--no-javaモード時は、Javaがnullとなるため
// no such functionエラーとなる.
clog('error!! ' + ex);
}
// メソッドの定義 #1
function myMethod1() {
clog('JavaScript内で定義したメソッドの呼び出し1');
}
//メソッドの定義 #2
function myMethod2(msgs) {
clog('JavaScript内で定義したメソッドの呼び出し2');
msgs.stream().map(function(x) {
return '☆' + x + '☆';
}).forEach(function(x) {
console.log(x);
});
msgs.add('xxx');
return msgs;
}
// オブジェクトによるインターフェイスの実装 #1
myAction = {
run: function() {
clog('JavaScriptによるインターフェイスの実装1: ★☆★');
}
};
// オブジェクトによるインターフェイスの実装 #2
myFunc = {
myMethod1: function(msg1) {
clog('JavaScriptによるインターフェイスの実装2a');
return '★' + msg1 + '★';
},
myMethod2: function(msg2) {
clog('JavaScriptによるインターフェイスの実装2b');
return '☆' + msg2 + '☆';
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment