OSコマンドと引数のインジェクション

外部入力によってJavaプロセスの実行ファイル、コマンド、オプション、引数が変わる問題です。

説明

外部入力で実行ファイルやコマンド文字列を選べると、攻撃者が別のプログラムを実行できるおそれがあります。実行ファイルが固定されていても、オプションや引数に入る入力によってプログラムの動作が変わる引数インジェクションが起こり得ます。findの-execのように別のプログラムを起動するオプションでは、OSコマンドの実行につながる可能性があります。

JavaのProcessBuilderは実行ファイルと引数を別々の文字列要素で渡します。シェルを明示的に呼び出さなければ、シェルのメタ文字の解釈を避けて引数の境界を保てますが、各引数の意味までは検証しません。実行ファイルを固定し、引数の意味も検証してください。

Runtime.exec(String)はJava 18から非推奨です。文字列を空白で分割し、シェルを自動的には呼び出しません。代わりに実行ファイルと引数を分けて渡してください。以下の例は、シェル、-c、外部のコマンド文字列を別々の要素として渡します。

Apache Commons Exec 1.6.0のCommandLine.addArguments(String)も引用符と空白を解析します。API文書では解析による意図しない影響を避けるため、コマンドラインを段階的に組み立てることを推奨しています。ただし、個別のaddArgument呼び出しも引数の安全性を検証するものではありません。

想定される影響

  • アプリケーションの権限による任意のコマンドやプログラムの実行
  • ファイル、認証情報、環境変数などの機密データの閲覧・変更
  • ネットワークへのアクセス、権限昇格の足掛かり、サービスの中断

影響は、子プロセスが継承するアカウント、ファイルシステム、ネットワークの権限によって異なります。

対処方法

  1. 可能なら外部プロセスを起動せず、同じ処理を行うJavaライブラリやAPIを使ってください。
  2. プロセスが必要なら実行ファイルのパスを固定し、ユーザーが選べる操作をサーバー管理の有限な引数テンプレートへ対応付けてください。
  3. 実行ファイルと各引数はProcessBuilderの別々の要素として渡してください。可変のオプションやオペランドは、コマンドと位置に合う厳格な許可リストで検証してください。関数名がsanitizeやescapeというだけでは安全とは判断できません。
  4. --は、対象ユーティリティがオプション終端としてサポートし、入力がオペランドの位置にある場合だけ使ってください。すべてのプログラムに通用する無害化手段ではありません。
  5. sh -c、cmd /c、PowerShellの-Command、python -cなど、シェルやインタープリターのコード位置に信頼できない値を渡さないでください。
  6. 子プロセスは必要最小限の権限で実行してください。

例

変更前

外部入力をPOSIXシェルのコマンド文字列として直接渡すため、シェル構文として解釈されます。

java
import java.io.IOException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class CommandController {
    @GetMapping("/run")
    void run(@RequestParam String command) throws IOException {
        new ProcessBuilder("/bin/sh", "-c", command).start();
    }
}

シェルを使わなくても、入力でプログラムのオプションを選べると動作が変わる可能性があります。

変更後

入力をコマンドラインへコピーせず、許可した操作識別子をサーバー管理の固定引数リストへ対応付けます。

java
import java.io.IOException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class OperationController {
    @GetMapping("/run")
    void run(@RequestParam String operation) throws IOException {
        String[] argv = switch (operation) {
            case "date" -> new String[] {"/usr/bin/date", "--iso-8601=seconds"};
            case "identity" -> new String[] {"/usr/bin/id", "-u"};
            default -> throw new IllegalArgumentException("Unsupported operation");
        };

        new ProcessBuilder(argv).start();
    }
}

ファイル名やホスト名などのデータ引数を渡す場合も、コマンドと引数の位置に合う形式、長さ、許可する値を検証してください。

参考資料