説明
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・メモリの枯渇とサービス拒否
対処方法
- SpEL式、Janinoスクリプト、JSR-223のソースは、アプリケーションが管理する固定文字列にします。信頼できない文字列をコードとして解析・コンパイル・評価しないでください。
- SpELでは式を固定し、リクエストの値は変数、ルートオブジェクトの単純なデータ、関数の引数としてだけ渡します。信頼できない式を
StandardEvaluationContextで評価せず、SimpleEvaluationContextもサニタイザーや安全性の保証として扱わないでください。 - Janinoでは固定スクリプトをコンパイルし、
setParametersで入力を宣言します。リクエストの値はソースに連結せず、evaluateの引数として渡します。 - JSR-223では固定スクリプトを評価し、リクエストの値は
BindingsまたはScriptContextで渡します。必要な値やオブジェクトだけを公開し、エンジンの選択をソースの検証とみなさないでください。 - ユーザーが操作を選ぶ場合は、サーバーが管理する有限の識別子を、確認済みの固定式やスクリプトに対応付けます。長さの確認、文字のエスケープ、
sanitizeというヘルパー名だけでは、実行入力の安全性を判断できません。 - ユーザーがコードを書く必要がある機能は、アプリケーション外の別プロセスやコンテナーに隔離します。最小権限と、実行時間、CPU、メモリ、ファイルシステム、ネットワークの制限を適用します。JDK 24以降ではSecurity Managerを有効にできないため、これやJaninoの古いサンドボックスの案内に依存しないでください。
例
SpEL
変更前
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の全機能を公開するコンテキストで評価されます。
変更後
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
変更前
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スクリプトの本体としてコンパイルされ、直ちに評価されます。
変更後
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
変更前
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のソースとして直ちに実行されます。実際の言語と利用できる機能は、注入されたエンジンやバインディングによって異なります。
変更後
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 のデータとしてだけ渡します。
適用時の注意点
解析・コンパイルと、実際の評価・実行を区別し、生成された式やコードがその後どう使われるかを確認してください。入力経路やヘルパー名が変わっても、信頼できない文字列を実行ソースとして扱うリスクは変わりません。
参考資料
- Spring Framework 7.0.9 — SpEL Security Considerations
- Spring Framework — ExpressionParser API
- Janino 3.1.12 — ScriptEvaluator source and API
- Janino 3.1.12 — ICookable source and API
- Java SE 25 — ScriptEngine API
- Java SE 25 — Compilable API
- Java SE 25 — Bindings API
- OpenJDK JEP 486 — Permanently Disable the Security Manager
- OWASP Top 10:2025 A05 — Injection
- OWASP Top 10:2021 A03 — Injection
- CWE-94 — Improper Control of Generation of Code