翻訳者から: CUBAプラットフォームの開発時に、このフレームワークにカスタムスクリプトを実行して、アプリケーションのビジネスロジックをより柔軟に構成できるようにしました。 この機会が良いか悪いか(そして私たちはCUBAについて話しているだけではありません)は長い間議論されていますが、ユーザースクリプトの実行を制御する必要があるという事実は問題になりません。 カスタムスクリプトの実行を管理するためのGroovyの便利な機能の1つが、セドリックシャンポーのこの翻訳で紹介されています。 彼が最近Groovy開発チームを辞めたという事実にもかかわらず、プログラマーのコミュニティは今後長い間彼の労力を利用するでしょう。
Groovyを使用するとランタイムでコードを簡単に動的に実行できるため、Groovyを使用する最も一般的に使用される方法の1つはスクリプトです。 アプリケーションに応じて、スクリプトはファイルシステム、データベース、リモートサービスなどのさまざまな場所に配置できますが、最も重要なのは、スクリプトを実行するアプリケーションの開発者が必ずしもそれらを記述する必要がないことです。 さらに、スクリプトは限られた環境(限られたメモリ、ファイル記述子の数の制限、ランタイム...)で動作するか、ユーザーがスクリプトのすべての言語機能を使用できないようにすることができます。
この投稿はあなたに教えてくれます。
- Groovyが内部DSLを書くのに適している理由
- アプリケーションのセキュリティの面でその機能は何ですか
- DSLを改善するためにコンパイルを構成する方法
-
SecureASTCustomizer
の価値について - 型制御拡張機能について
- 型制御拡張機能を使用してサンドボックスを効果的にする方法
たとえば、ユーザーが数式を計算できるようにするために必要なことを想像してください。 実装オプションの1つは、内部DSLを埋め込み、パーサーを作成し、最後にこれらの式のインタープリターを作成することです。 もちろん、これを行うには作業が必要になりますが、たとえば、インタープリターで式を計算したり、ランタイムで生成されたクラスのキャッシングを使用する代わりに式のバイトコードを生成するなど、パフォーマンスを改善する必要がある場合、Groovyは素晴らしいオプションです
ドキュメントには多くのオプションが説明されていますが、最も簡単な例はEval
クラスを使用することです。
Example.java
int sum = (Integer) Eval.me("1+1");
1+1
コードは解析され、バイトコードにコンパイルされ、実行時にGroovyによってロードおよび実行されます。 もちろん、このサンプルのコードは非常に単純であり、パラメーターを追加する必要がありますが、考えは実行可能コードは任意であるということです。 そして、それはまさにあなたが必要とするものではないかもしれません。 計算機では、次のようなものを許可する必要があります。
1+1 x+y 1+(2*x)**y cos(alpha)*r v=1+x
確かにそうではない
println 'Hello' (0..100).each { println 'Blah' } Pong p = new Pong() println(new File('/etc/passwd').text) System.exit(-1) Eval.me('System.exit(-1)') // a script within a script!
ここから困難が始まり、いくつかの問題を解決する必要があることが明らかになります。
- 言語の文法をその機能のサブセットに制限する
- ユーザーがコードを実行できないようにする
- 悪意のあるコードの実行を防ぐ
電卓の例は非常に単純ですが、より複雑なDSLの場合、特にDSLが開発者が使用できないほど単純な場合、問題のあるコードを書いていることに気付かないかもしれません。
数年前、私はこのような状況にありました。 言語学者によって書かれたGroovy「スクリプト」を実行するエンジンを開発しました。 たとえば、問題の1つは、エンドレスループが誤って作成される可能性があることです。 サーバーでコードが実行され、CPUを100%消費するスレッドが現れた後、アプリケーションサーバーを再起動する必要がありました。 DSL、ツール、アプリケーションのパフォーマンスに影響を与えずに問題を解決する方法を模索する必要がありました。
実際、多くの人々が同様のニーズを持っています。 過去4年間、私は同じ質問をしている多くの人々と話してきました。Groovyスクリプトでユーザーがナンセンスなことをしないようにするにはどうすればよいでしょうか?
カスタマイズコンパイラ
当時、私はすでに自分で決断を下し、他の人も同様の何かを開発したことを知っていました。 最後に、Guillaume Laforgeは、これらの問題の解決に役立つメカニズムをGroovyカーネルに作成することを提案しました。 Groovy 1.8.0では、 コンパイルカスタマイザーとして登場しました。
コンパイルカスタマイザーは、Groovyスクリプトのコンパイルプロセスを変更する一連のクラスです。 独自のカスタマイザーを作成できますが、Groovyは以下を提供します。
- ユーザーがインポートの説明を追加する必要がないように、インポートカスタマイザーがスクリプトに暗黙的にインポートを追加する
- カスタマイザーAST(抽象構文ツリー)変換。AST変換をスクリプトに直接追加できます。
- 言語の文法と構文構成を制限する安全なASTカスタマイザー
AST変換のカスタマイザーは、 @ThreadInterrupt
変換による無限ループの問題を解決するのに役立ちましたが、ほとんどの場合、 SecureASTCustomizerはおそらく最も誤解されるものです。
申し訳ありません。 それから私はよりよい名前を思い付くことができなかった。 「SecureASTCustomizer」という名前の最も重要な部分はASTです。 このメカニズムの目的は、特定のAST関数へのアクセスを制限することでした。 タイトルの「セキュア」という言葉は一般に不要です。その理由を説明します。 「Deadly Groovy SecureASTCustomizer」というタイトルのJenkinsで有名な川口康介によるブログ投稿もあります。 そして、すべてが非常に正しくそこに書かれています。 SecureASTCustomizerは、サンドボックス用に設計されていません。 コンパイル時の言語を制限するために作成されましたが、実行は制限されていません。 今、私は最高の名前がGrammarCustomizerになると思います。 しかし、ご存じのとおり、コンピューターサイエンスには3つの困難があります。キャッシュの無効化、名前の発明、ユニットごとのエラーです。
ここで、スクリプトのセキュリティを確保する手段として安全なASTカスタマイザーを検討していることを想像してください。タスクは、ユーザーがスクリプトからSystem.exit
をSystem.exit
ないようにするSystem.exit
。 ドキュメンテーションには、ブラックリストまたはホワイトリストを作成することにより、特別な受信者で通話を禁止できることが記載されています。 セキュリティが必要な場合は、許可されるものを厳密に記載するホワイトリストを常にお勧めしますが、何も禁止するブラックリストはお勧めしません。 ハッカーは常にあなたが考慮しなかったかもしれないことについて考えるからです。 例を挙げましょう。
SecureASTCustomizer
を使用してプリミティブサンドボックススクリプトエンジンをSecureASTCustomizer
方法はSecureASTCustomizer
です。 Groovyで作成することもできますが、統合コードとスクリプトの違いがより明確になるように、Javaの構成例を示します。
public class Sandbox { public static void main(String[] args) { CompilerConfiguration conf = new CompilerConfiguration(); SecureASTCustomizer customizer = new SecureASTCustomizer(); customizer.setReceiversBlackList(Arrays.asList(System.class.getName())); conf.addCompilationCustomizers(customizer); GroovyShell shell = new GroovyShell(conf); Object v = shell.evaluate("System.exit(-1)"); System.out.println("Result = " +v); } }
- コンパイラー構成を作成する
- 安全なASTカスタマイザーを作成する
- メソッド呼び出しの受信者としての
System
クラスがブラックリストに登録されていることを宣言する - コンパイラー構成にカスタマイザーを追加する
- 構成をシェルスクリプトにバインドします。つまり、サンドボックスを作成しようとします。
- 「悪い」スクリプトを実行する
- スクリプトの実行結果を表示する
このクラスを実行すると、スクリプトの実行中にエラーが発生します。
General error during canonicalization: Method calls not allowed on [java.lang.System] java.lang.SecurityException: Method calls not allowed on [java.lang.System]
この結論は、 System
クラスのメソッドの実行を許可しない安全なASTカスタマイザを備えたアプリケーションによって発行されます。 成功! スクリプトを保護しました! しかし、ちょっと待って...
SecureASTCustomizerがハッキングされています!
保護、言う? しかし、これを行うとどうなりますか:
def c = System c.exit(-1)
プログラムを再度実行すると、エラーなしでクラッシュし、画面に結果が表示されないことがわかります。 プロセス終了コードは-1です。これは、ユーザースクリプトが実行されたことを意味します。 どうした コンパイル時に、安全なASTカスタマc.exit
は、 c.exit
がASTレベルで機能するため、原則としてSystem
メソッドの呼び出しであることを認識できません! メソッド呼び出しを分析します。この場合、メソッド呼び出しはc.exit(-1)
で、受信者を決定し、受信者がホワイト(またはブラック)リストにあるかどうかを確認します。 この場合、受信者はc
、この変数はdefを介して宣言され、これはObject
として宣言するのと同じであり、セキュアASTカスタマイザーは変数c
型がSystem
ではなくObject
であると考えSystem
!
一般に、セキュアASTカスタマイザーで作成されたさまざまな構成を回避する方法は多数あります。 ここにいくつかのクールなものがあります:
((Object)System).exit(-1) Class.forName('java.lang.System').exit(-1) ('java.lang.System' as Class).exit(-1) import static java.lang.System.exit exit(-1)
さらに多くの可能性があります。 Groovyの動的な性質により、コンパイル時にこれらの問題を修正することができません。 ただし、ソリューションが存在します。 1つのオプションは、標準のJVMセキュリティマネージャーに依存することです。 ただし、これはシステム全体ですぐに使用できる重量のある大量のソリューションであり、スズメで大砲を発射するのと同じです。 さらに、すべての場合に機能するわけではありません。たとえば、ファイルの読み取りを禁止するが作成はしない場合などです。
この制限は、むしろ私たちの多くにとっては悔しさであり、 実行中のチェックに基づいたソリューションの作成につながりました。 このタイプのチェックには、このような問題はありません。 たとえば、メソッド呼び出しの検証を開始する前に、メッセージの実際の受信者タイプがわかるためです。 特に興味深いのは、次の実装です。
- ジム・ホワイトによるSecureScript
- 川口耕介によるGroovy Sandbox
- サイモン・テンプルによるGroovy Sandbox
ただし、これらの実装はどれも完全に信頼でき安全ではありません。 たとえば、Kosukeのバージョンは、内部コールサイトキャッシング実装のハックに基づいています。 問題は、introdynamicバージョンのGroovyと互換性がなく、これらの内部クラスがGroovyの将来のバージョンに含まれないことです。 一方、SimonのバージョンはAST変換に基づいていますが、多くの潜在的なホールを残しています。
その結果、私の友人Corinne Crisch、Fabrizia Matrat、Sebastian Blanc、そして私はランタイムで新しいサンドボックスメカニズムを作成することに決めました。これらのプロジェクトのような問題はありません。 ニースのハッカソンで実装を開始し、昨年のGreachカンファレンスで報告しました 。 このメカニズムはAST変換に基づいており、各メソッドの呼び出し、クラスフィールドへのアクセス、変数のインクリメント、バイナリ式の前にチェックするコードを本質的に書き換えます。この実装はまだ準備が整っておらず、まだ多くの作業が行われていません。 「暗黙のこれ」を介して呼び出されるメソッドとパラメーターの問題は、たとえばビルダーのようにまだ解決されていないことに気付きました。
xml { cars { // cars is a method call on an implicit this: "this".cars(...) car(make:'Renault', model: 'Clio') } }
これまでのところ、Groovyのメタオブジェクトプロトコルのアーキテクチャにより、この問題を解決する方法はまだ見つかりませんでした。これは、別のレシーバーに切り替える前にメソッドが見つからないときにレシーバーが例外をスローするという事実に基づいています。 つまり、これは、実際のメソッド呼び出しの前にレシーバーのタイプを知ることができないことを意味します。 そして、呼び出しが合格した場合、手遅れです...
そして、最近まで、実行可能スクリプトが言語の動的プロパティを使用する場合、この問題に対する最適な解決策がありませんでした。 しかし、今こそ、言語のダイナミズムを少し犠牲にする準備ができている場合に、状況を大幅に改善する方法を説明するときです。
型チェック
SecureASTCustomizerの主な問題に戻りましょう。これは抽象的な構文ツリーで動作し、特定のタイプやメッセージの受信者に関する情報はありません。 しかし、GroovyはGroovy 2からコンパイルを追加し、Groovy 2.1 では型チェック用の拡張機能を追加しました。
型チェックの拡張機能は非常に強力です。GroovyDSL開発者はコンパイラーの型推論を支援し、通常は発生しない場合にコンパイルエラーを生成することもできます。 これらの拡張機能は、たとえば特性やマークアップテンプレートエンジンを実装する場合など、静的コンパイラをサポートするためにGroovyによって内部的に使用されます。
パーサーの結果を使用する代わりに、型チェックメカニズムからの情報に依存できるとしたらどうでしょうか。 ハッカーが記述しようとしたコードを取得します。
((Object)System).exit(-1)
型チェックを有効にすると、コードはコンパイルされません。
1 compilation error: [Static type checking] - Cannot find matching method java.lang.Object#exit(java.lang.Integer). Please check if the declared type is right and if the method exists.
そのため、このコードはコンパイルされなくなりました。 そして、このコードを取得した場合:
def c = System c.exit(-1)
ご覧のとおり、型チェックに合格し、メソッドにラップされ、 groovy
コマンドを使用して実行されます。
@groovy.transform.TypeChecked // or even @CompileStatic void foo() { def c = System c.exit(-1) } foo()
型チェッカーは、 exit
メソッドがSystem
クラスから呼び出され、有効であることを検出します。 これはここでは役に立ちません。 しかし、このコードが型チェックに合格した場合、コンパイラが型System
レシーバーへの呼び出しを認識することを知っていSystem
。 一般に、アイデアは、型チェックのための内線付きの呼び出しを禁止することです。
型チェックの簡単な拡張
サンドボックスの詳細を調べる前に、型チェック用の標準拡張機能を使用してスクリプトを「保護」してみましょう。 このような拡張機能の登録は簡単です。 @CompileStatic
注釈(または静的コンパイルを使用している場合は@CompileStatic
のextensions
パラメーターを設定するだけです。
@TypeChecked(extensions=['SecureExtension1.groovy']) void foo() { def c = System c.exit(-1) } foo()
拡張機能は、ソースコード形式のクラスパスで検索されます(型チェック用にプリコンパイル済みの拡張機能を作成できますが、この記事では考慮しません)。
SecureExtension1.groovy
onMethodSelection { expr, methodNode -> if (methodNode.declaringClass.name=='java.lang.System') { addStaticTypeError("Method call is not allowed!", expr) } }
- 型チェッカーが呼び出すメソッドを選択するとき
- メソッドがクラス
System
属する場合 - タイプチェッカーにエラーを生成させます
必要なのはそれだけです。 コードを再度実行すると、コンパイルエラーが表示されます!
/home/cchampeau/tmp/securetest.groovy: 6: [Static type checking] - Method call is not allowed! @ line 6, column 3. c.exit(-1) ^ 1 error
今回は、型チェッカーのおかげで、 c
System
クラスのインスタンスとして認識され、呼び出しを禁止できます。 これは非常に単純な例であり、構成に関してセキュアASTカスタマイザーを使用して実行できるすべてを示すものではありません。 私たちが書いた拡張機能では 、チェックはハードコーディングされていますが、カスタマイズ可能にする方が良いかもしれません。 それでは、例をもっと複雑にしましょう。
アプリケーションがドキュメントの特定のメトリックを計算し、ユーザーがそれらをカスタマイズできると仮定します。 この場合、DSL:
- (少なくとも)変数
score
を操作します - ユーザーが数学演算を実行できるようにします( cos 、 abs 、...メソッドの呼び出しを含む)
- 他のすべての方法を禁止する必要があります
サンプルユーザースクリプト:
abs(cos(1+score))
このDSLは簡単に設定できます。 これは上で定義したものの変形です。
Sandbox.java
CompilerConfiguration conf = new CompilerConfiguration(); ImportCustomizer customizer = new ImportCustomizer(); customizer.addStaticStars("java.lang.Math"); conf.addCompilationCustomizers(customizer); Binding binding = new Binding(); binding.setVariable("score", 2.0d); GroovyShell shell = new GroovyShell(binding,conf); Double userScore = (Double) shell.evaluate("abs(cos(1+score))"); System.out.println("userScore = " + userScore);
-
import static java.lang.Math.*
をすべてのスクリプトに追加するimportカスタマイザーを追加しimport static java.lang.Math.*
- スクリプトで
score
変数を使用可能にする - スクリプトを実行する
スクリプトを毎回解析およびコンパイルする代わりに、スクリプトをキャッシュする方法があります。 詳細については、ドキュメントを参照してください。
したがって、スクリプトは機能しますが、ハッカーが悪意のあるコードを起動することを妨げるものは何もありません。 型チェックを使用する予定なので、 @CompileStatic
変換の使用をお勧めします。
- スクリプトで型チェックを有効にし、型チェックの拡張機能のおかげで追加のチェックを実行できます
- スクリプトのパフォーマンスを改善する
暗黙的に@CompileStatic
アノテーションをスクリプトに追加するのは非常に簡単です。 コンパイラー構成を更新するだけです。
ASTTransformationCustomizer astcz = new ASTTransformationCustomizer(CompileStatic.class); conf.addCompilationCustomizers(astcz);
スクリプトを再度実行しようとすると、コンパイルエラーが表示されます。
Script1.groovy: 1: [Static type checking] - The variable [score] is undeclared. @ line 1, column 11. abs(cos(1+score)) ^ Script1.groovy: 1: [Static type checking] - Cannot find matching method int#plus(java.lang.Object). Please check if the declared type is right and if the method exists. @ line 1, column 9. abs(cos(1+score)) ^ 2 errors
どうした コンパイラの観点からスクリプトを読むと、変数「スコア」について何も知らないことが明らかになります。 しかし、開発者として、 あなたはこれがdouble
変数であることを知っていますが、コンパイラはそれを出力できません。 このために、型チェック用の拡張機能が作成されます。コンパイラーに追加情報を与えると、コンパイルは正常に機能します。 この場合、 score
変数のタイプがdouble
ことを示す必要があります。
したがって、 @CompileStatic
アノテーションの@CompileStatic
方法をわずかに変更できます。
ASTTransformationCustomizer astcz = new ASTTransformationCustomizer( singletonMap("extensions", singletonList("SecureExtension2.groovy")), CompileStatic.class);
これは、 @CompileStatic(extensions=['SecureExtension2.groovy'])
注釈されたコードを「エミュレート」します。 もちろん、 score
変数を認識する拡張機能を作成する必要があります。
SecureExtension2.groovy
unresolvedVariable { var -> if (var.name=='score') { return makeDynamic(var, double_TYPE) } }
- 型チェッカーが変数を定義できない場合
- 変数名が
score
- コンパイラーに
double
型の変数を動的に定義させます
型チェック用のDSL拡張機能の詳細な説明は、ドキュメントのこのセクションにありますが、複合コンパイルモードの例があります。コンパイラはscore
変数を決定できません。 あなたは、DSL開発者として、変数が実際にその型makeDynamic
ことを知っているので、 makeDynamic
へのmakeDynamic
ここにあります:「OK、心配しないで、私は何をしているのか知っています。この変数はdouble
型」 以上です!
最初に完成した「安全な」拡張機能
それでは、すべてをまとめましょう。 System
クラスのメソッドの呼び出しを防止する型チェック拡張機能と、 score
変数を定義する別の型チェック拡張機能を作成しました。 したがって、それらを接続すると、型チェック用の最初の完全な拡張機能が得られます。
SecureExtension3.groovy
// disallow calls on System onMethodSelection { expr, methodNode -> if (methodNode.declaringClass.name=='java.lang.System') { addStaticTypeError("Method call is not allowed!", expr) } } // resolve the score variable unresolvedVariable { var -> if (var.name=='score') { return makeDynamic(var, double_TYPE) } }
Javaクラスの構成を更新して、型チェックに新しい拡張機能を使用することを忘れないでください。
ASTTransformationCustomizer astcz = new ASTTransformationCustomizer( singletonMap("extensions", singletonList("SecureExtension3.groovy")), CompileStatic.class);
コードを再度実行します-それでも動作します。 今これを試してください:
abs(cos(1+score)) System.exit(-1)
スクリプトのコンパイルはエラーでクラッシュします:
Script1.groovy: 1: [Static type checking] - Method call is not allowed! @ line 1, column 19. abs(cos(1+score));System.exit(-1) ^ 1 error
おめでとうございます、あなたは悪意のあるコードの実行を防ぐ最初の型チェック拡張機能を書きました!
拡張設定の強化
したがって、すべてが順調に進んでおり、 System
クラスのメソッドの呼び出しを禁止できますが、新しい脆弱性がすぐに発見されるようであり、悪意のあるコードの起動を防ぐ必要があります。 そのため、拡張機能のすべてをハードコーディングする代わりに、拡張機能をユニバーサルでカスタマイズ可能にしようとします。 型チェックのために拡張機能にコンテキストを渡す直接的な方法がないため、これはおそらく最も困難です。 したがって、この考え方は、スレッドローカル変数(曲線メソッド、はい)を使用して構成データを型チェッカーに渡すことに基づいています。
まず、変数のリストをカスタマイズ可能にします。 Javaコードは次のようになります。
Sandbox.java
public class Sandbox { public static final String VAR_TYPES = "sandboxing.variable.types"; public static final ThreadLocal<Map<String, Object>> COMPILE_OPTIONS = new ThreadLocal<>(); public static void main(String[] args) { CompilerConfiguration conf = new CompilerConfiguration(); ImportCustomizer customizer = new ImportCustomizer(); customizer.addStaticStars("java.lang.Math"); ASTTransformationCustomizer astcz = new ASTTransformationCustomizer( singletonMap("extensions", singletonList("SecureExtension4.groovy")), CompileStatic.class); conf.addCompilationCustomizers(astcz); conf.addCompilationCustomizers(customizer); Binding binding = new Binding(); binding.setVariable("score", 2.0d); try { Map<String,ClassNode> variableTypes = new HashMap<String, ClassNode>(); variableTypes.put("score", ClassHelper.double_TYPE); Map<String,Object> options = new HashMap<String, Object>(); options.put(VAR_TYPES, variableTypes); COMPILE_OPTIONS.set(options); GroovyShell shell = new GroovyShell(binding, conf); Double userScore = (Double) shell.evaluate("abs(cos(1+score));System.exit(-1)"); System.out.println("userScore = " + userScore); } finally { COMPILE_OPTIONS.remove(); } } }
-
ThreadLocal
, - —
SecureExtension4.groovy
-
variableTypes
— “ → ” -
score
-
options
— - "variable types" VAR_TYPES
- thread local
- , , thread local
:
import static Sandbox.* def typesOfVariables = COMPILE_OPTIONS.get()[VAR_TYPES] unresolvedVariable { var -> if (typesOfVariables[var.name]) { return makeDynamic(var, typesOfVariables[var.name]) } }
- thread local
- , ,
- type checker
thread local, , type checker . , unresolvedVariable
, , , type checker, . , . !
. , .
. , . , , . , System.exit
, :
java.lang.System#exit(int)
, Java, :
public class Sandbox { public static final String WHITELIST_PATTERNS = "sandboxing.whitelist.patterns"; // ... public static void main(String[] args) { // ... try { Map<String,ClassNode> variableTypes = new HashMap<String, ClassNode>(); variableTypes.put("score", ClassHelper.double_TYPE); Map<String,Object> options = new HashMap<String, Object>(); List<String> patterns = new ArrayList<String>(); patterns.add("java\\.lang\\.Math#"); options.put(VAR_TYPES, variableTypes); options.put(WHITELIST_PATTERNS, patterns); COMPILE_OPTIONS.set(options); GroovyShell shell = new GroovyShell(binding, conf); Double userScore = (Double) shell.evaluate("abs(cos(1+score));System.exit(-1)"); System.out.println("userScore = " + userScore); } finally { COMPILE_OPTIONS.remove(); } } }
-
java.lang.Math
:
import groovy.transform.CompileStatic import org.codehaus.groovy.ast.ClassNode import org.codehaus.groovy.ast.MethodNode import org.codehaus.groovy.ast.Parameter import org.codehaus.groovy.transform.stc.ExtensionMethodNode import static Sandbox.* @CompileStatic private static String prettyPrint(ClassNode node) { node.isArray()?"${prettyPrint(node.componentType)}[]":node.toString(false) } @CompileStatic private static String toMethodDescriptor(MethodNode node) { if (node instanceof ExtensionMethodNode) { return toMethodDescriptor(node.extensionMethodNode) } def sb = new StringBuilder() sb.append(node.declaringClass.toString(false)) sb.append("#") sb.append(node.name) sb.append('(') sb.append(node.parameters.collect { Parameter it -> prettyPrint(it.originType) }.join(',')) sb.append(')') sb } def typesOfVariables = COMPILE_OPTIONS.get()[VAR_TYPES] def whiteList = COMPILE_OPTIONS.get()[WHITELIST_PATTERNS] onMethodSelection { expr, MethodNode methodNode -> def descr = toMethodDescriptor(methodNode) if (!whiteList.any { descr =~ it }) { addStaticTypeError("You tried to call a method which is not allowed, what did you expect?: $descr", expr) } } unresolvedVariable { var -> if (typesOfVariables[var.name]) { return makeDynamic(var, typesOfVariables[var.name]) } }
-
MethodNode
- thread local
- ,
, :
Script1.groovy: 1: [Static type checking] - You tried to call a method which is not allowed, what did you expect?: java.lang.System#exit(int) @ line 1, column 19. abs(cos(1+score));System.exit(-1) ^ 1 error
, ! , , . , ! , , . , ( foo.text
, foo.getText()
).
すべてをまとめる
, type checker' "property selection", , . , , . , , — . .
SandboxingTypeCheckingExtension.groovy
import groovy.transform.CompileStatic import org.codehaus.groovy.ast.ClassCodeVisitorSupport import org.codehaus.groovy.ast.ClassHelper import org.codehaus.groovy.ast.ClassNode import org.codehaus.groovy.ast.MethodNode import org.codehaus.groovy.ast.Parameter import org.codehaus.groovy.ast.expr.PropertyExpression import org.codehaus.groovy.control.SourceUnit import org.codehaus.groovy.transform.sc.StaticCompilationMetadataKeys import org.codehaus.groovy.transform.stc.ExtensionMethodNode import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport import org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport import static Sandbox.* class SandboxingTypeCheckingExtension extends GroovyTypeCheckingExtensionSupport.TypeCheckingDSL { @CompileStatic private static String prettyPrint(ClassNode node) { node.isArray()?"${prettyPrint(node.componentType)}[]":node.toString(false) } @CompileStatic private static String toMethodDescriptor(MethodNode node) { if (node instanceof ExtensionMethodNode) { return toMethodDescriptor(node.extensionMethodNode) } def sb = new StringBuilder() sb.append(node.declaringClass.toString(false)) sb.append("#") sb.append(node.name) sb.append('(') sb.append(node.parameters.collect { Parameter it -> prettyPrint(it.originType) }.join(',')) sb.append(')') sb } @Override Object run() { // Fetch white list of regular expressions of authorized method calls def whiteList = COMPILE_OPTIONS.get()[WHITELIST_PATTERNS] def typesOfVariables = COMPILE_OPTIONS.get()[VAR_TYPES] onMethodSelection { expr, MethodNode methodNode -> def descr = toMethodDescriptor(methodNode) if (!whiteList.any { descr =~ it }) { addStaticTypeError("You tried to call a method which is not allowed, what did you expect?: $descr", expr) } } unresolvedVariable { var -> if (isDynamic(var) && typesOfVariables[var.name]) { storeType(var, typesOfVariables[var.name]) handled = true } } // handling properties (like foo.text) is harder because the type checking extension // does not provide a specific hook for this. Harder, but not impossible! afterVisitMethod { methodNode -> def visitor = new PropertyExpressionChecker(context.source, whiteList) visitor.visitMethod(methodNode) } } private class PropertyExpressionChecker extends ClassCodeVisitorSupport { private final SourceUnit unit private final List<String> whiteList PropertyExpressionChecker(final SourceUnit unit, final List<String> whiteList) { this.unit = unit this.whiteList = whiteList } @Override protected SourceUnit getSourceUnit() { unit } @Override void visitPropertyExpression(final PropertyExpression expression) { super.visitPropertyExpression(expression) ClassNode owner = expression.objectExpression.getNodeMetaData(StaticCompilationMetadataKeys.PROPERTY_OWNER) if (owner) { if (expression.spreadSafe && StaticTypeCheckingSupport.implementsInterfaceOrIsSubclassOf(owner, classNodeFor(Collection))) { owner = typeCheckingVisitor.inferComponentType(owner, ClassHelper.int_TYPE) } def descr = "${prettyPrint(owner)}#${expression.propertyAsString}" if (!whiteList.any { descr =~ it }) { addStaticTypeError("Property is not allowed: $descr", expression) } } } } }``` sandbox', assert' , , : ``Sandbox.java`` ```java public class Sandbox { public static final String WHITELIST_PATTERNS = "sandboxing.whitelist.patterns"; public static final String VAR_TYPES = "sandboxing.variable.types"; public static final ThreadLocal<Map<String, Object>> COMPILE_OPTIONS = new ThreadLocal<Map<String, Object>>(); public static void main(String[] args) { CompilerConfiguration conf = new CompilerConfiguration(); ImportCustomizer customizer = new ImportCustomizer(); customizer.addStaticStars("java.lang.Math"); ASTTransformationCustomizer astcz = new ASTTransformationCustomizer( singletonMap("extensions", singletonList("SandboxingTypeCheckingExtension.groovy")), CompileStatic.class); conf.addCompilationCustomizers(astcz); conf.addCompilationCustomizers(customizer); Binding binding = new Binding(); binding.setVariable("score", 2.0d); try { Map<String, ClassNode> variableTypes = new HashMap<String, ClassNode>(); variableTypes.put("score", ClassHelper.double_TYPE); Map<String, Object> options = new HashMap<String, Object>(); List<String> patterns = new ArrayList<String>(); // allow method calls on Math patterns.add("java\\.lang\\.Math#"); // allow constructors calls on File patterns.add("File#<init>"); // because we let the user call each/times/... patterns.add("org\\.codehaus\\.groovy\\.runtime\\.DefaultGroovyMethods"); options.put(VAR_TYPES, variableTypes); options.put(WHITELIST_PATTERNS, patterns); COMPILE_OPTIONS.set(options); GroovyShell shell = new GroovyShell(binding, conf); Object result; try { result = shell.evaluate("Eval.me('1')"); // error assert false; } catch (MultipleCompilationErrorsException e) { System.out.println("Successful sandboxing: "+e.getMessage()); } try { result = shell.evaluate("System.exit(-1)"); // error assert false; } catch (MultipleCompilationErrorsException e) { System.out.println("Successful sandboxing: "+e.getMessage()); } try { result = shell.evaluate("((Object)Eval).me('1')"); // error assert false; } catch (MultipleCompilationErrorsException e) { System.out.println("Successful sandboxing: "+e.getMessage()); } try { result = shell.evaluate("new File('/etc/passwd').getText()"); // getText is not allowed assert false; } catch (MultipleCompilationErrorsException e) { System.out.println("Successful sandboxing: "+e.getMessage()); } try { result = shell.evaluate("new File('/etc/passwd').text"); // getText is not allowed assert false; } catch (MultipleCompilationErrorsException e) { System.out.println("Successful sandboxing: "+e.getMessage()); } Double userScore = (Double) shell.evaluate("abs(cos(1+score))"); System.out.println("userScore = " + userScore); } finally { COMPILE_OPTIONS.remove(); } } }
おわりに
Groovy JVM. , . , , , . , Groovy, sandboxing' (, , ).
, , . , . , , .
, sandboxing', , — SecureASTCustomizer
. , , : secure AST customizer , (, ), ( , ).
, : , , . Groovy . Groovy, , - pull request, - !