Java書けるんなら当然JavaScript呼び出せるよね?

上司「Java書けるんなら当然JavaScript呼び出せるよね?さっさとやっておいて」

JavaからJavaScriptを呼び出す

Java 6 からスクリプトAPIを通じてスクリプト言語を呼び出すことができる。Java標準でJavaScriptのエンジン Rhinoが組み込まれているので特にインストール作業やクラスパスを通すような作業なしにJavaScriptの呼び出しをすることができる。

import javax.script.*;

public class Sample {
	public static void main(String[] args) throws ScriptException {
		ScriptEngineManager manager = new ScriptEngineManager();
		ScriptEngine engine = manager.getEngineByName("js");
		
		engine.eval("println('Hello Script API');");
	}
}

セキュリティ上の問題

文字列でJavaScriptのコードを渡せば呼び出すことが簡単に出来るわけだが、RhinoからはJavaAPIも簡単に呼び出すことが出来る。

import javax.script.*;

public class Sample {
	public static void main(String[] args) throws ScriptException {
		ScriptEngineManager manager = new ScriptEngineManager();
		ScriptEngine engine = manager.getEngineByName("js");
		
		engine.eval("println('Hello Script API');" +
				"java.lang.System.exit(0)");

		System.out.println("end");
	}
}

そして、JavaAPIのうち、セキュリティ的に凶悪なSystem.exit()とかが普通に呼べてしまう。これはJVMを停止する命令で、外からスクリプト文字列を受け取ってRhinoで呼び出すといった使い方をする場合、重大なセキュリティ懸念となる。そうじゃなくてもやりたい放題ってのは困るんだけど。

セキュリティマネージャ

そこでセキュリティマネージャで権限をコントロールする。

AccessController.doPrivileged()を使うことで、指定権限で動作させることができる。

import java.security.*;

public class Sample2 {
	public static void main(String[] args) {
		System.out.println("start");
		
		// 外側のコードのセキュリティポリシー
		// このサンプルではデフォルト設定を使う
		Policy policy = new Policy() {};
		Policy.setPolicy(policy);

		// setSecurityManagerをするとセキュリティマネージャが有効になる
		System.setSecurityManager(new SecurityManager());

		// 権限を確認したい場合はコメントを外す
		// ProtectionDomain pd = Sample2.class.getProtectionDomain();
		// System.out.println(pd);
		
		PermissionCollection permissions = new Permissions();
		// ここで必要なパーミッションがあれば追加する
		// permissions.add(new RuntimePermission("exitVM"));

		// AccessControlContextを作る
		CodeSource codeSource = Sample2.class.getProtectionDomain().getCodeSource();
		ProtectionDomain domain = new ProtectionDomain(codeSource, permissions);
		ProtectionDomain[] domains = new ProtectionDomain[]{domain};
		AccessControlContext context = new AccessControlContext(domains);

		// 権限を絞って動作させる処理のラッパー
		PrivilegedExceptionAction<Void> action = new MyPrivilegedExceptionAction();

		// 引数で渡したAccessControlContextがもつパーミッションと、
		// 現在のパーミッションとの共通部分のパーミッションで実行される
		try {
			AccessController.doPrivileged(action, context);
		} catch (PrivilegedActionException e) {
			// 例外が発生した場合
			e.printStackTrace();
		}
	}

	/** 権限を絞って動作させる処理のラッパー */
	static class MyPrivilegedExceptionAction implements PrivilegedExceptionAction<Void> {

		/** このメソッドが指定の権限で動作する */
		@Override
		public Void run() throws Exception {
			System.out.println("in MyPrivilegedAction");

			ScriptEngineManager manager = new ScriptEngineManager();
			ScriptEngine engine = manager.getEngineByName("js");
			
			// スクリプト内でSystem.exit()を呼ぶ
			engine.eval("println('Hello Script API');" +
					"java.lang.System.exit(0)");

			// exit()されるのでここは実行されない
			System.out.println("end");
			return null;
		}
	}
}

MyPrivilegedExceptionAction に記載されたスクリプト呼び出し部分はSystem.exitする権限を持たない。そのため、実行すると例外が発生し、PrivilegedActionExceptionのcatchに進むだろう。

コメントを外して"exitVM"権限を与えて実行すると、スクリプト内のSystem.exit()が成功し、そこで処理が終了することが分かるだろう。