JNDIインジェクション

信頼できないJNDI検索名によるリソースインジェクション

説明

JNDIは名前を使ってオブジェクトやコンテキストを検索します。lookup、lookupLink、InitialContext.doLookupの名前に信頼できない値を渡すと、攻撃者がアプリケーションに解決させるリソース識別子を選べます。

InitialContextはscheme_id:...形式の文字列をURL名として扱い、そのスキームのURLコンテキストファクトリーを探します。LDAP、RMI、DNSや独自のプロバイダーを通じて、意図しない接続、リソースへのアクセス、情報漏えい、遅延が起こり得ます。返された参照からオブジェクトを再構成する際には、JDK、プロバイダー、ファクトリー、クラスパス、直列化設定によってコード実行に至る場合もあります。

新しいJDKの保護にも適用範囲があります。JDK 24は組み込みLDAP/RMIプロバイダーによるリモートのファクトリークラスのダウンロードを恒久的に無効化し、従来のtrustURLCodebaseプロパティを削除しました。JDK 25の組み込みLDAPプロバイダーは、com.sun.jndi.ldap.object.trustSerialDataを明示的にtrueにしない限り、直列化データや関連する参照を再構成しません。ただし、URL名による接続や、ローカルファクトリー、独自のプロバイダー、ObjectFactoryBuilderの危険性は残ります。

想定される影響

  • 攻撃者が選んだLDAP、RMI、DNS、独自プロバイダーへのサーバー側接続
  • 内部の名前サービスへのアクセスや、ファイアウォール内の情報の漏えい
  • 遅い、または応答しないプロバイダーによるスレッド・接続資源の枯渇
  • 危険な旧設定、直列化、ファクトリー構成でのオブジェクト生成の副作用やコード実行

対処方法

  1. 完全なJNDI名を開発者が管理してください。外部からは業務上のキーを受け取り、閉じたswitch、enumなどで固定のローカル名に対応付けます。信頼できない接尾辞を連結しないでください。
  2. 名前を直接受け取る必要がある場合は、確認済みの完全なローカル名の有限集合と完全一致で比較してください。接頭辞、正規表現、ldap:やrmi:などのスキームの拒否リストだけでは、すべてのプロバイダーや連合先を確認できません。
  3. Context.INITIAL_CONTEXT_FACTORY、Context.PROVIDER_URL、Context.OBJECT_FACTORIES、Context.URL_PKG_PREFIXESなどを信頼できる配備設定で固定してください。クラスパスには信頼できるプロバイダーとファクトリーだけを置き、独自のObjectFactoryBuilderは別途確認します。
  4. サポート対象の更新済みJDK 25を使用してください。削除されたtrustURLCodebaseの設定は対策になりません。LDAPのtrustSerialDataは有効にせず、jdk.jndi.object.factoriesFilterとプロバイダー固有のフィルターを必要なファクトリーに限定してください。
  5. 送信先を必要な名前サーバーに絞り、最小権限の認証情報を使ってください。組み込みLDAPではcom.sun.jndi.ldap.connect.timeoutとcom.sun.jndi.ldap.read.timeoutを設定します。これらは影響を抑えますが、攻撃者が指定する名前を安全にはしません。

例

変更前

java
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.http.HttpServletRequest;

class UnsafeJndiLookup {
    Object lookup(HttpServletRequest request) throws NamingException {
        String name = request.getParameter("name");
        Context context = new InitialContext();
        return context.lookup(name); // BAD: リクエスト値がプロバイダーとリソースを選択できる
    }
}

変更後

外部のキーを、アプリケーションが管理する完全なローカル名に変換します。switchの結果にはリクエストの文字列を含めません。

java
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.http.HttpServletRequest;

class SafeJndiLookup {
    Object lookup(HttpServletRequest request) throws NamingException {
        String key = request.getParameter("name");
        if (key == null) {
            throw new NamingException("resource not selected");
        }

        String jndiName = switch (key) {
            case "mainDs" -> "java:comp/env/jdbc/MainDS";
            case "ordersQ" -> "java:comp/env/jms/OrdersQueue";
            default -> throw new NamingException("resource not allowed");
        };

        return new InitialContext().lookup(jndiName);
    }
}

参考資料