Created
May 23, 2014 12:47
-
-
Save seraphy/8cdcf11bf2da40fb908d to your computer and use it in GitHub Desktop.
Java8のNashornをサンドボックスで評価する。カスタムポリシークラスもしくはポリシーファイルによるポリシーの設定方法と、undocumentedなNashornのオプションである --no-java の利用によるJavaコードの制限方法についての例。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
// } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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"; | |
}; | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// エンジンに設定されたグローバル関数の呼び出し | |
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