JEXLインジェクション

JEXLの式、スクリプト、テンプレートへのインジェクション

説明

Apache Commons JEXLは文字列を式やスクリプトとして解析し、後でコンテキストのデータとともに実行します。JXLTはJEXLを使うテンプレートエンジンです。信頼できないテキストがソースになると、攻撃者がアプリケーションの実行する演算や制御フローを記述できます。

影響はJEXLのバージョン、機能、権限、名前空間、公開するコンテキストオブジェクト、実際の実行有無によって変わります。業務判断の変更、公開オブジェクトへのアクセス、過剰な資源消費が起こり得ます。広い権限や古い設定では、ファイル、ネットワーク、リフレクション、クラス読み込み、プロセス実行まで可能になる場合があります。

JEXL 3.7.0は既定の権限をSECUREに変更し、new(...)、グローバルな副作用、pragma、annotationなどを既定で無効にしました。ただし公式文書は、SECUREもRESTRICTEDも信頼できない入力を実行するための完全なサンドボックスではないと明記しています。スクリプトのループは既定でも許可されています。

想定される影響

  • 評価結果を使う認証、価格計算、フィルターなどの業務判断の変更
  • コンテキスト、名前空間、許可されたクラスが公開するデータの参照や変更
  • 広い権限を持つエンジンからの機密性の高いAPIへのアクセス
  • 長いソース、ループ、広い範囲、重い計算によるCPU・メモリ・出力容量の枯渇

対処方法

  1. 式、スクリプト、テンプレートのソースは開発者が管理する固定文字列にしてください。リクエスト値はJexlContextのデータや宣言済みスクリプト引数として渡し、ソースに連結しないでください。
  2. 利用者が処理を選ぶ場合は、アプリケーション側の有限のキーを、確認済みの固定プログラムやJava関数に対応付けてください。単純な計算には、より小さな文法の専用パーサーを優先します。
  3. 利用者によるJEXL記述が必要なら、サポート対象の版でJexlPermissions.NONEまたはJexlPermissions.create(...)から始め、必要なクラスとメンバーだけを許可してください。UNRESTRICTEDや古い既定値に戻さず、SECUREやRESTRICTEDだけで十分と考えないでください。
  4. JexlFeatures.createNone()から必要な構文だけを有効にし、コンテキスト、名前空間、import、算術拡張、クラスローダーへのアクセスを最小限にしてください。独自のJexlBuilder.uberspect(...)はビルダーの権限制御を置き換えるため、別途確認が必要です。
  5. ソースの長さ・複雑さ、実行時間、CPU、メモリ、出力量を制限してください。cancellable(true)と期限でのスレッド割り込みを設定し、強い隔離が必要なら別プロセスやコンテナーで実行します。
  6. 正規表現やトークンの拒否リストで任意のJEXLソースを安全にはできません。許可する構文とオブジェクトの機能を限定し、その許可判断をテストしてください。

例

変更前

java
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlScript;
import org.apache.commons.jexl3.MapContext;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class UnsafeJexlController {
    @PostMapping("/execute")
    Object execute(@RequestParam("script") String script) {
        JexlEngine engine = new JexlBuilder().create();
        JexlScript compiled = engine.createScript(script); // BAD: リクエストをコードとして解析
        return compiled.execute(new MapContext());
    }
}

JEXL 3.7の既定値は以前より厳格ですが、リクエスト全体がプログラムとして解釈されます。ループを含めることができ、追加の権限やコンテキストオブジェクトによって操作範囲も広がります。

変更後

ソースを固定し、外部の値をコンテキストデータとして分離します。

java
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlExpression;
import org.apache.commons.jexl3.MapContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class SafeJexlController {
    private final JexlEngine engine = new JexlBuilder().create();

    @GetMapping("/total")
    double total(@RequestParam("price") double price,
                 @RequestParam("quantity") int quantity) {
        MapContext context = new MapContext();
        context.set("price", price);
        context.set("quantity", quantity);

        JexlExpression fixed = engine.createExpression("price * quantity");
        return ((Number) fixed.evaluate(context)).doubleValue();
    }
}

利用者が構文を記述する必要がある場合は、次の抜粋のように権限と機能を閉じた状態から始められます。ただし入力制限や資源の隔離は別途必要です。

java
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlFeatures;
import org.apache.commons.jexl3.introspection.JexlPermissions;

JexlEngine constrained = new JexlBuilder()
        .permissions(JexlPermissions.create()) // 既定ですべてのリフレクションアクセスを拒否
        .features(JexlFeatures.createNone())   // 必要な構文だけを明示的に追加
        .cancellable(true)
        .strict(true)
        .silent(false)
        .create();

参考資料