Groovyコードインジェクション

Groovyコードインジェクション

説明

Groovyコードインジェクションは、信頼できない文字列がGroovyのソースコードとして解釈されると発生します。Eval、GroovyShell.evaluate/run、Script.evaluate は、渡された式やスクリプトを実行します。GroovyShell.parse と GroovyClassLoader.parseClass はソースを実行可能なスクリプトやクラスにコンパイルし、その段階でもGrapeによる依存関係の解決やAST変換などの機能が動作する場合があります。

攻撃者がソースを制御すると、アプリケーションの権限でデータやファイルを読み書きし、内部ネットワークにアクセスしたりプロセスを起動したりできるおそれがあります。無限ループや過剰な割り当てによってリソースを使い果たすことも可能です。特定の文字や既知のメソッド名を削除するだけでは、Groovyの構文やメタプログラミングを十分に制限できず、安全なサニタイズにはなりません。

想定される影響

  • アプリケーションの権限での任意のコードやOSコマンドの実行
  • ファイル、環境変数、認証情報、データベース、内部サービスへのアクセス
  • アプリケーションデータの改ざんや権限境界の回避
  • CPU、メモリ、スレッド、出力の過剰な使用によるサービス拒否

対処方法

  1. Groovyのソースは開発者が管理する固定文字列にします。可能ならJavaの処理に置き換え、またはユーザーが選べる有限のキーを、確認済みの関数やスクリプトに対応付けます。
  2. 固定スクリプトでリクエストの値を使う場合は、Binding や Eval の値引数を通してデータとして渡します。ソース文字列に連結したり埋め込んだりしないでください。
  3. ユーザー作成コードが必要な場合は、アプリケーションのプロセス外にある別のプロセス、コンテナー、仮想マシンで実行します。アプリケーションの秘密情報、不要なマウント、ネットワークアクセスを除き、非特権アカウントを使います。CPU、メモリ、プロセス数、出力、実行時間を制限し、上限を超えた実行を終了させます。
  4. SecureASTCustomizer は構文を制限する多層防御の手段であり、完全なサンドボックスではありません。不要なGrapeやコンパイル時の機能を無効にし、許可する機能を最小限にします。独自の ClassLoader の拒否リストやJava Security Managerをセキュリティ境界にしないでください。JDK 24以降、Security Managerは恒久的に無効化されています。
  5. サポートされている安定版のGroovyと最新のセキュリティパッチを使用します。スクリプトの識別子や実行結果を監査し、機密性のあるソースやバインド値はログに残さないでください。

例

変更前

java
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class UnsafeGroovyController {
    @PostMapping("/exec")
    Object execute(@RequestParam("script") String userScript) {
        Script script = new GroovyShell().parse(userScript); // BAD: ユーザー入力をコンパイル
        return script.run();
    }
}

parse は後で実行する Script を作る処理であり、安全性を検証するものではありません。コンパイル時の機能が run() より前に動作する場合もあるため、呼び出しを遅らせても隔離にはなりません。

変更後

可能なら動的な言語実行をなくし、サーバーが管理する操作だけを選べるようにします。

java
import java.util.Map;
import java.util.function.IntUnaryOperator;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class SafeController {
    private static final Map<String, IntUnaryOperator> OPERATIONS = Map.of(
            "double", value -> value * 2,
            "square", value -> value * value);

    @GetMapping("/calculate")
    int calculate(@RequestParam("operation") String operation,
                  @RequestParam("value") int value) {
        IntUnaryOperator selected = OPERATIONS.get(operation);
        if (selected == null) {
            throw new IllegalArgumentException("unsupported operation");
        }
        return selected.applyAsInt(value);
    }
}

Groovyが必要でも、ソースが固定されていれば、外部の値をバインディングのデータとして分離できます。

java
import groovy.lang.Binding;
import groovy.lang.GroovyShell;

Object doubleValue(int value) {
    Binding binding = new Binding();
    binding.setVariable("value", value);
    return new GroovyShell(binding).evaluate("return value * 2");
}

参考資料