SpEL、Janino、Java JSR-223のコードインジェクション

信頼できないSpEL、Janino、Java JSR-223実行コード入力

説明

JavaやKotlinで、信頼できない文字列をSpEL式、Janinoスクリプト、JSR-223のスクリプトソースとして使うと、コードインジェクションのリスクが生じます。

SpELはコンストラクターやメソッドを呼び出し、プロパティ、フィールド、Spring Beanを参照できます。Janinoは渡されたJava文をバイトコードにコンパイルします。JSR-223は選択したエンジンの言語でソースを実行したり、再利用できる形式にコンパイルしたりします。ScriptEngine.eval は即座に実行しますが、SpELの解析、Janinoのコンパイル、Compilable.compile では、その後の評価によってコードが実行されます。エンジンや公開されたコンテキスト・バインディングによっては、攻撃者のコードがアプリケーションと同じ権限で動くおそれがあります。

Spring Framework 7.0.9のセキュリティ指針では、信頼できないSpEL式の評価は本質的に危険で、原則として避けるべきとしています。StandardEvaluationContext はSpELの全機能を公開します。SimpleEvaluationContext の制限もベストエフォートであり、安全性を保証するものではありません。

Java SE 25の ScriptEngine APIは、スクリプトのソースと、Bindings や ScriptContext で渡す値・オブジェクトを区別しています。信頼できない値は、固定スクリプトのデータとして渡してください。エンジン名を制限したり信頼できる実装を選んだりするだけでは、信頼できないスクリプトソースは安全になりません。

想定される影響

  • アプリケーションの権限での任意のコードや危険なメソッドの実行
  • ファイル、データベース、認証情報などの漏えいや改ざん
  • プロセスの実行、ネットワークアクセス、権限昇格の足がかり
  • 過剰なコンパイルや評価によるCPU・メモリの枯渇とサービス拒否

対処方法

  1. SpEL式、Janinoスクリプト、JSR-223のソースは、アプリケーションが管理する固定文字列にします。信頼できない文字列をコードとして解析・コンパイル・評価しないでください。
  2. SpELでは式を固定し、リクエストの値は変数、ルートオブジェクトの単純なデータ、関数の引数としてだけ渡します。信頼できない式を StandardEvaluationContext で評価せず、SimpleEvaluationContext もサニタイザーや安全性の保証として扱わないでください。
  3. Janinoでは固定スクリプトをコンパイルし、setParameters で入力を宣言します。リクエストの値はソースに連結せず、evaluate の引数として渡します。
  4. JSR-223では固定スクリプトを評価し、リクエストの値は Bindings または ScriptContext で渡します。必要な値やオブジェクトだけを公開し、エンジンの選択をソースの検証とみなさないでください。
  5. ユーザーが操作を選ぶ場合は、サーバーが管理する有限の識別子を、確認済みの固定式やスクリプトに対応付けます。長さの確認、文字のエスケープ、sanitize というヘルパー名だけでは、実行入力の安全性を判断できません。
  6. ユーザーがコードを書く必要がある機能は、アプリケーション外の別プロセスやコンテナーに隔離します。最小権限と、実行時間、CPU、メモリ、ファイルシステム、ネットワークの制限を適用します。JDK 24以降ではSecurity Managerを有効にできないため、これやJaninoの古いサンドボックスの案内に依存しないでください。

例

SpEL

変更前

java
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class DocumentController {
    private final SpelExpressionParser parser = new SpelExpressionParser();

    @GetMapping("/document")
    String evaluate(@RequestParam String expressionText) {
        Expression expression = parser.parseExpression(expressionText);
        return expression.getValue(new StandardEvaluationContext(), String.class);
    }
}

リクエストの値が式の構文として解析され、SpELの全機能を公開するコンテキストで評価されます。

変更後

java
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class DocumentController {
    private static final Expression TEXT_EXPRESSION =
        new SpelExpressionParser().parseExpression("#text");

    @GetMapping("/document")
    String render(@RequestParam String text) {
        StandardEvaluationContext context = new StandardEvaluationContext();
        context.setVariable("text", text);
        return TEXT_EXPRESSION.getValue(context, String.class);
    }
}

式はアプリケーションで固定し、リクエストの値は変数のデータとしてだけ渡します。

Janino

変更前

java
import org.codehaus.janino.ScriptEvaluator;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class ScriptController {
    @PostMapping("/script")
    Object run(@RequestParam String script) throws Exception {
        ScriptEvaluator evaluator = new ScriptEvaluator();
        evaluator.cook(script);
        return evaluator.evaluate();
    }
}

リクエストの値がJavaスクリプトの本体としてコンパイルされ、直ちに評価されます。

変更後

java
import org.codehaus.janino.ScriptEvaluator;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class ScriptController {
    @PostMapping("/normalize")
    String normalize(@RequestParam String text) throws Exception {
        ScriptEvaluator evaluator = new ScriptEvaluator();
        evaluator.setReturnType(String.class);
        evaluator.setParameters(
            new String[] { "input" },
            new Class<?>[] { String.class }
        );
        evaluator.cook("return input.trim();");
        return (String) evaluator.evaluate(new Object[] { text });
    }
}

コンパイルするスクリプトは固定し、リクエストの値は宣言済みパラメーターの実行時引数としてだけ渡します。

JSR-223

変更前

java
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class AutomationController {
    private final ScriptEngine engine;

    AutomationController(ScriptEngine engine) {
        this.engine = engine;
    }

    @PostMapping("/automation")
    Object run(@RequestParam String script) throws ScriptException {
        return engine.eval(script);
    }
}

リクエストの値がJSR-223のソースとして直ちに実行されます。実際の言語と利用できる機能は、注入されたエンジンやバインディングによって異なります。

変更後

java
import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class AutomationController {
    private static final String FIXED_SCRIPT = "input";
    private final ScriptEngine engine;

    AutomationController(ScriptEngine engine) {
        this.engine = engine;
    }

    @PostMapping("/automation")
    Object run(@RequestParam String input) throws ScriptException {
        Bindings bindings = engine.createBindings();
        bindings.put("input", input);
        return engine.eval(FIXED_SCRIPT, bindings);
    }
}

スクリプトは、選択したエンジンの言語に合ったサーバー管理の定数です。リクエストの値は Bindings のデータとしてだけ渡します。

適用時の注意点

解析・コンパイルと、実際の評価・実行を区別し、生成された式やコードがその後どう使われるかを確認してください。入力経路やヘルパー名が変わっても、信頼できない文字列を実行ソースとして扱うリスクは変わりません。

参考資料