改行文字によるログインジェクション

ログインジェクション

説明

信頼できないデータを、レコードの区切り文字を無害化せずに行単位のログへ記録すると、ログインジェクションが発生します。キャリッジリターン(\r、CR)やラインフィード(\n、LF)でレコードの境界を作成・変更されると、偽のログの挿入、解析の妨害、調査の誤誘導につながります。

Pythonの logging APIは、位置引数(args)を msg に埋め込んでメッセージを作ります。logging.info("User: %s", user) のように書式引数を分けても、user に含まれるCR/LFは自動では無害化されません。

想定される影響

  • ログの偽造: 偽の行を挿入し、重大度や形式が異なる記録に見せかけることができます。
  • 自動解析の妨害: レコード形式が崩れ、収集処理やSIEMの解析・アラートに影響する可能性があります。
  • 調査の信頼性低下: 時系列やユーザーの活動記録が不正確になり、調査や対応が遅れるおそれがあります。

対処方法

  • 信頼できないメッセージや書式引数をロガーに渡す前に、CRとLFの両方を改行にならない文字へ置き換えます。
  • 区切り文字をエスケープし、イベントごとに1レコードを出力するシリアライザーやフォーマッターを使います。キーと値の組にするだけでは、安全な構造化ログにはなりません。
  • 標準のコンパクトな json.dumps(...) は、CR、LF、U+0000〜U+001Fの制御文字をエスケープします。行単位のログでは indent による整形や、攻撃者が変更できるシリアライズ設定を使いません。
  • 許可リストだけで対処する場合は、すべての候補が信頼できる有限のリテラル値で、CR/LFを含まないことが必要です。形式や長さの検証だけでは区切り文字を無害化できません。
  • ログをHTMLで表示する場合は、ビューアーの出力時にHTMLエンコーディングも適用します。HTMLエスケープでは、保存されるログのCR/LFは無害化されません。

例

変更前

python
import logging
from flask import Flask, request

app = Flask(__name__)

@app.route('/search')
def search():
    term = request.args.get('term', '')
    # 書式引数を分けてもtermのCR/LFは残ります。
    logging.info("Search term: %s", term)
    return "ok"

変更後

CR/LFの置換

python
import logging
from flask import Flask, request

app = Flask(__name__)

@app.route('/search')
def search_safe():
    term = request.args.get('term', '')
    safe_term = term.replace("\r", " ").replace("\n", " ")
    logging.info("Search term: %s", safe_term)
    return "ok"

コンパクトなJSONへのシリアライズ

python
import json
import logging
from flask import request

def log_search():
    term = request.args.get('term', '')
    logging.info(json.dumps({"event": "search", "term": term}))

解説:

  • 変更前: term のCR/LFが書式処理後も残り、1つのイベントが複数の記録に見えるおそれがあります。
  • CR/LFの置換: 両方の区切り文字を置き換えてから記録します。
  • コンパクトなJSON: 標準の json.dumps(...) は文字列内の制御文字をエスケープします。indent は出力自体を複数行にするため、行単位のログでは指定しません。

適用時の注意点

レコードの境界は、最終的なハンドラーとフォーマッターの設定に左右されます。実際の出力でCR/LFが適切に処理されることを確認し、ログビューアーではHTMLエンコーディングも適用してください。

参考資料