ログインジェクション

信頼できないテキストによるログレコード境界の偽装を防ぐ

説明

ログインジェクションは、信頼できないテキストを出力形式に合わせて処理せず記録すると発生します。行単位のログでは、キャリッジリターン(\r)や改行(\n)によって、一つのイベントを複数のレコードに見せかけられます。CSV、JSON、HTML、端末出力では、それぞれの区切り文字や制御文字も処理する必要があります。

logger.info("login user={}", user)のようなパラメーター化されたロギングでも、値に含まれるCR/LFが自動で無害化される保証はありません。実際のエンコードは、ロギングプロバイダー、レイアウト、収集基盤、ビューアーに依存します。

想定される影響

  • 監査イベントの偽装や攻撃の痕跡の隠蔽
  • アラート、検索、集計、フォレンジック処理でのレコード構造の破損
  • ログビューアーでの二次的なインジェクションや運用者の判断ミス
  • 過度に長い入力を記録することによる保存容量や処理資源の消費

長さの制限は資源消費を抑える補助策であり、それだけではレコードの偽装を防げません。

対処方法

  1. 最終出力形式に合わせて値を構造的にエンコードする、サポート対象のプロバイダーとレイアウトを優先してください。攻撃文字列を使い、収集基盤やビューアーまで含めて動作を確認します。
  2. 行単位のテキストでは、メッセージとすべての置換引数に含まれる\rと\nを除去するか、改行しない可視表現に置き換えてください。プレースホルダーだけでは代用できません。
  3. 共通の処理は確認済みのヘルパーやフィルターにまとめてください。小さな閉じた集合の値なら、不変の厳密な許可リストも使えます。長さの制限はレコード区切りの処理後に適用します。
  4. パスワード、セッション識別子、アクセストークンは記録しないでください。必要な識別情報も一貫した方法で削除・マスクします。

Apache Commons TextのStringEscapeUtils.escapeJavaはCR/LFなどをJavaのエスケープ表現に変換するため、行単位のテキストでレコード分割を防ぐ用途に使えます。ただしCSV、HTML、端末などに共通する汎用エンコーダーではありません。

例

変更前

プレースホルダーを使っていますが、untrustedUserに含まれる改行をそのままロギングプロバイダーへ渡します。

java
import jakarta.servlet.http.HttpServletRequest;
import java.util.logging.Level;
import java.util.logging.Logger;

final class VulnerableAuditLogExample {
    private static final Logger LOGGER =
            Logger.getLogger(VulnerableAuditLogExample.class.getName());

    static void recordFailedLogin(HttpServletRequest request) {
        String untrustedUser = request.getParameter("username");
        LOGGER.log(Level.WARNING, "Failed login for {0}", untrustedUser);
    }
}

変更後

行単位のテキストに記録する前に、すべてのCR/LFを除去します。実際には中央のヘルパーや出力レイアウトにまとめてください。

java
import jakarta.servlet.http.HttpServletRequest;
import java.util.logging.Level;
import java.util.logging.Logger;

final class SafeAuditLogExample {
    private static final Logger LOGGER =
            Logger.getLogger(SafeAuditLogExample.class.getName());

    static void recordFailedLogin(HttpServletRequest request) {
        String untrustedUser = request.getParameter("username");
        String safeUser = untrustedUser == null ? "" : untrustedUser.replaceAll("[\\r\\n]", "");
        LOGGER.log(Level.WARNING, "Failed login for {0}", safeUser);
    }
}

replaceAll("\\r\\n", "")でCRLFの組だけを除去したり、CRとLFの片方だけを除去したりすると、単独の改行文字が残るため不十分です。

参考資料

サポートが終了したLog4j 1.xではなく、サポート対象のロギングライブラリーを使ってください。