【実務・中級編】 HTTPヘッダーインジェクションの防止とレスポンス分割攻撃対策 – 暗号理論・認証基盤 & エンドポイントセキュリティ防御ガイド

おい、ちょっと手を止めてくれ。
コードレビューをしていると、未だに「ユーザーからの入力をそのままHTTPレスポンスヘッダーに突っ込んでいるコード」を見かけることがある。今回は、HTTPヘッダーインジェクションと、そこから派生するレスポンス分割攻撃(CRLFインジェクション)について話をしよう。

暗号理論や認証基盤の強固な設計も、こうした「トランスポート層の入り口」で足元をすくわれたら一巻の終わりだ。RSAやAESでどれだけ強固に暗号化通信(TLS)を守っていようが、アプリケーション層のロジックがヘッダーの改行制御を許してしまえば、セッションハイジャックもクロスサイト・スクリプティング(XSS)も自由自在に成立してしまう。

現場の第一線で戦うエンジニアとして、なぜこの脆弱性が生まれ、攻撃者がどこを突いてくるのか、そしてどうやって完全に叩き潰すのかを実務目線で徹底的に解説しよう。

—

1. 脆弱性の本質:なぜ「改行」一つでシステムが崩壊するのか

HTTPプロトコルは非常にシンプルだ。ヘッダーとボディの区切り、そして各ヘッダーの終端は、キャリッジリターン(CR = \r)とラインフィード(LF = \n)の組み合わせ、すなわち \r\n によって定義されている。

攻撃者がユーザー入力値を通じてこの \r\n(URLエンコード表現では %0d%0a)をサーバー側に送り込み、それをHTTPレスポンスヘッダー(例えば Location や Set-Cookie)にそのまま出力してしまったらどうなるか。

ブラウザやリバースプロキシは、インジェクションされた改行を「ヘッダーの終了」と誤認する。その結果、意図しない新しいヘッダーが勝手に追加されたり、あるいはヘッダーエリアが強制終了されて、それ以降の入力値がそのまま「HTTPレスポンスのボディ(HTMLやJavaScript)」として解釈されてしまうのだ。これがレスポンス分割攻撃(HTTP Response Splitting)の正体である。

攻撃のシミュレーション(PoCの概念)

例えば、リダイレクト処理を行う脆弱なアプリケーションを想像してほしい。

HTTP/1.1 302 Found
Location: /welcome?user=USER_INPUT_HERE

ここに、攻撃者が次のようなペイロードを送り込んだとする。

%0d%0aContent-Type: text/html%0d%0a%0d%0a<script>alert(document.cookie)</script>

これを埋め込まれたレスポンスは、Webサーバーからブラウザに対して次のように送信されてしまう。

HTTP/1.1 302 Found
Location: /welcome?user=
Content-Type: text/html

<script>alert(document.cookie)</script>

見事にレスポンスが分割され、前半はリダイレクトヘッダーのつもりだったものが、後半で完全に独立したHTMLドキュメント(今回はXSSの実行)にすり替わっている。TLSで暗号化されていようが、WAFが適切にチューニングされていなければ、このアプリケーション層の脆弱性は綺麗にすり抜けてしまう。

—

2. 現場で使えるセキュアコーディング実装例

では、この脆弱性を完全に封じ込めるにはどうすればいいか。
答えは極めてシンプルだ。「ユーザー入力をHTTPヘッダーに直接含めないこと」。どうしても含める必要がある場合は、改行文字(\r, \n)を厳格に検知して即座にエラーとするか、完全に除去・エスケープすることだ。単なる置換ではなく、インジェクションの兆候があるリクエストは不正アクセスとして弾くのがセキュア設計の基本となる。

以下に、実務でそのまま使えるPHPとPython(Flask)のセキュアな実装サンプルを示す。

PHPによるセキュアなリダイレクト・ヘッダー出力の実装

PHPで header() 関数を使用する際の実装例だ。入力値に改行コードが含まれているかを正規表現で厳密にチェックし、含まれていた場合は処理を中断する。

<?php
/**
 * セキュアにリダイレクトヘッダーを出力する関数
 * 
 * @param string $url 遷移先のURL(ユーザー入力を含む可能性を考慮)
 * @return void
 */
function secureRedirect(string $url): void {
    // 改行コード(CR、LF)が含まれているかチェックする
    // URLエンコードされた表現(%0d, %0a)や、実際の制御文字を同時に検知する
    if (preg_match('/[\r\n]|%0d|%0a/i', $url)) {
        // 攻撃の兆候とみなし、ログに記録して処理をアボートする
        error_log("Security Alert: HTTP Header Injection attempt detected: " . $url);
        
        http_response_code(400);
        header('Content-Type: text/plain; charset=UTF-8');
        echo "Bad Request: Invalid characters detected in header value.";
        exit;
    }

    // パスカルケースや意図しないプロトコル(javascript:など)の混入を防ぐための追加バリデーション
    // 同一ドメイン内へのリダイレクトに限定する場合はパース検証を推奨
    $parsedUrl = parse_url($url);
    if (isset($parsedUrl['scheme']) && !in_array(strtolower($parsedUrl['scheme']), ['http', 'https'], true)) {
        http_response_code(400);
        echo "Bad Request: Unsafe URL scheme.";
        exit;
    }

    // 安全が確認された値のみヘッダーに渡す
    header('Location: ' . $url, true, 302);
    exit;
}

// --- 実行例 ---
// $userInput = $_GET['redirect'];
// secureRedirect($userInput);

Python (Flask) による実装例

PythonのWebフレームワークでも同様だ。レスポンスオブジェクトを生成する際に、値のバリデーションを挟む。

from flask import Flask, request, make_response, abort
import re
from urllib.parse import urlparse

app = Flask(__name__)

# 改行文字の検知用正規表現パターン
CRLF_PATTERN = re.compile(r'[\r\n]|%0d|%0a', re.IGNORECASE)

@app.route('/redirect')
class SafeRedirect:
    pass

@app.route('/login')
def login():
    target_url = request.args.get('next', '/')
    
    # 1. ヘッダーインジェクション(CRLF)の検知
    if CRLF_PATTERN.search(target_url):
        # ログアウトや監査ログへの記録処理をここに実装
        app.logger.warning(f"CRLF Injection detected in redirect parameter: {target_url}")
        abort(400, description="Invalid characters in header value.")
        
    # 2. オープンリダイレクト対策(相対パスまたは自ドメインのみ許可)
    parsed = urlparse(target_url)
    if parsed.netloc and parsed.netloc != request.host:
        abort(400, description="External redirection is not allowed.")

    response = make_response("", 302)
    response.headers['Location'] = target_url
    return response

if __name__ == '__main__':
    app.run(debug=False)

—

3. インフラ・ミドルウェア層での多層防御(Defense in Depth)

アプリケーションコードの修正はもちろん必須だが、プロとしてのセキュリティエンジニアなら、インフラストラクチャ層でも二重・三重の備えをしておくべきだ。

近年のモダンなWebサーバー(NginxやApache)の多くは、デフォルトで不正な改行を含むリクエストラインやヘッダーを拒否するようになっているが、アップストリーム(リバースプロキシ)とバックエンドの挙動の差異を突かれるケースが後を絶たない。

Nginxにおける設定の要点

Nginxをリバースプロキシとして運用する場合、不正な文字を含むヘッダーをバックエンドに転送しないように設定することが重要だ。

server {
    listen 80;
    server_name example.com;

    # Nginxはデフォルトで制御文字(%00-%1Fなど)を含むリクエストを拒否するが、
    # アップストリームへ渡す際のヘッダークリーニングを明示的に行う
    
    location / {
        proxy_pass http://backend_cluster;
        
        # ユーザーから受け取った不正な改行を含む可能性のあるヘッダーをサニタイズ
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        
        # 不正な文字を含むHTTPヘッダーの転送を防ぐディレクティブの確認
        # ignore_invalid_headers はデフォルトで on になっていることを確認する
        ignore_invalid_headers on;
    }
}

—

4. チーフからの実践アドバイス

HTTPヘッダーインジェクションは、概念自体は古くから存在するものの、フレームワークの内部処理やレガシーなカスタムミドルウェアの改修漏れによって、現代のモダンなWebアプリケーションでも時折顔を出す厄介な脆弱性だ。

「入力値をそのままログに出力する処理」や「動的に生成するレスポンスヘッダー」を実装するときは、常に「ここに \r\n をねじ込まれたら、ブラウザはどう解釈するだろうか?」という攻撃者の視点を持ってコードに向き合ってほしい。

セキュリティは、派手な暗号アルゴリズムの選定だけでなく、こうした泥臭いプロトコルの仕様に基づいた入力検証の積み重ねによってのみ成り立っている。チームメンバーのコードレビューを行う際は、ぜひこのポイントを厳しくチェックしてほしい。頼りにしてるぞ。

コメント

タイトルとURLをコピーしました