【実務・中級編】HTTPレスポンス分割攻撃(HTTP Response Splitting)のメカニズムと防御 – HTTPプロトコル・通信規格実践ガイド

潜む脅威、HTTPレスポンス分割攻撃: CRLFインジェクションの深淵と鉄壁の防御策

やあ、諸君。今日もネットワークの深淵を覗きに来てくれたのか。素晴らしい。俺は長年、このデジタルな血管網の血流を管理してきた。パケットが生き物のように駆け巡る様、時にはその動きがおかしくなり、原因究明に何日も徹夜した夜もあった。今日は、そんな経験の中から、特に厄介で、しかし理解すれば怖くない、HTTPレスポンス分割攻撃についてじっくり話そうと思う。

Web APIを設計する者、インフラを運用する者にとって、HTTPはもはや呼吸するようなものだろう。しかし、その日常に潜む見えない落とし穴が、この「HTTPレスポンス分割攻撃(HTTP Response Splitting)」だ。名前だけ聞くと、なんだか大掛かりな攻撃に聞こえるかもしれないが、その根幹にあるのは、実は「CRLFインジェクション」という、比較的シンプルな脆弱性なんだ。

CRLFインジェクションとは何か? パケットの「改行」が招く悲劇

まず、この攻撃の核心であるCRLFインジェクションから紐解いていこう。CRLFとは、Carriage Return (CR, `\r`) と Line Feed (LF, `\n`) の組み合わせで、HTTPヘッダーにおける行の区切り文字としてRFCで定められている。

例えば、HTTP/1.1でのリクエストヘッダーは、以下のような構造になっている。

GET /index.html HTTP/1.1
Host: example.com
User-Agent: MyBrowser/1.0

見ての通り、ヘッダーフィールドは `キー: 値` の形式で、各ヘッダーはCRLFで区切られている。そして、ヘッダーの最後もCRLFで締めくくられ、その後に空行(CRLF)が入る。この空行が、ヘッダーとボディの境界を示す重要な合図なんだ。

さて、CRLFインジェクションとは、このCRLFを悪意を持ってレスポンスに挿入する攻撃だ。具体的には、Webアプリケーションがユーザーからの入力を適切にサニタイズ(無害化)せずに、そのままHTTPレスポンスのヘッダーやボディの一部として出力してしまう場合に発生する。

もし、アプリケーションがユーザーからの入力 `HOGE\r\n\r\nHTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\nFake Content` を、例えば `Location` ヘッダーの値としてそのまま出力してしまったらどうなるか?

本来、サーバーからのレスポンスはこうあるべきだ。

HTTP/1.1 302 Found
Location: http://example.com/some/path

しかし、CRLFインジェクションによって、レスポンスは以下のように「分割」されてしまう。

HTTP/1.1 302 Found
Location: HOGE
<-- ここで改行された! -->
HTTP/1.1 200 OK
Content-Type: text/html

Fake Content

見ての通り、本来一つのレスポンスであるべきものが、CRLFによって強制的に二つのレスポンスに分断されてしまった。これが「HTTPレスポンス分割」のメカニズムだ。攻撃者は、この二つ目のレスポンスに悪意のあるコンテンツを仕込むことで、様々な被害をもたらすことができる。

HTTPレスポンス分割の恐ろしさ: キャッシュ汚染からXSSまで

では、このレスポンス分割、具体的にどんな悪さを働くのだろうか? 主な脅威は以下の二つに大別できる。

1. キャッシュ汚染 (Cache Poisoning)

WebサーバーやCDN、ブラウザは、パフォーマンス向上のためにHTTPレスポンスをキャッシュする。攻撃者は、このキャッシュ機構を悪用する。

例えば、脆弱なアプリケーションが、ユーザーからの入力 `HOGE\r\n\r\nHTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n` を、`Host` ヘッダーの値として出力してしまったとしよう。

本来、`Host` ヘッダーはリクエストに含まれるもので、レスポンスヘッダーとして出力されるのはおかしい。しかし、もしアプリケーションがそれをそのまま `Content-Location` ヘッダーなどに含めてしまった場合、以下のようなレスポンスが生成される。

HTTP/1.1 200 OK
Content-Location: HOGE

<-- ここで改行 -->
HTTP/1.1 200 OK
Content-Type: text/html

この二つ目のレスポンスが、本来は安全なコンテンツであるべきリソースのキャッシュとして保存されてしまう。その結果、他のユーザーがそのリソースにアクセスするたびに、本来表示されるべきコンテンツではなく、攻撃者が仕込んだ悪意のあるJavaScriptが実行されてしまうのだ。これは、Webサイト全体の信頼性を大きく損なう、非常に危険な攻撃だ。

2. クロスサイトスクリプティング (Cross-Site Scripting – XSS)

レスポンス分割は、直接的なXSS攻撃のトリガーにもなり得る。上記の例で見たように、攻撃者は二つ目のレスポンスとしてHTMLやJavaScriptを注入できる。

例えば、ユーザーが `/profile` というURLにアクセスしたとする。本来なら、そのユーザーのプロフィール情報が表示されるべきだ。しかし、アプリケーションがユーザー名などの入力を適切にエスケープせずにレスポンスヘッダー(例えば `X-User-Name`)として出力してしまうと、以下のような攻撃が可能になる。

ユーザーが `http://vulnerable.com/profile?name=` のようなURLにアクセスしたとする。
アプリケーションがこれを `` という名前として処理し、`X-User-Name` ヘッダーにそのまま出力してしまう。

HTTP/1.1 200 OK
X-User-Name: <-- ここがおかしい!


Profile

Welcome, !

"; // ここに攻撃者のスクリプトが注入されている!
document.getElementById('user-name').innerHTML = userName;


もし、フロントエンドのJavaScriptが、この `X-User-Name` ヘッダーの値をそのままHTMLに埋め込むような処理をしていた場合、攻撃者のスクリプトが実行され、ユーザーのCookie情報が盗まれる可能性がある。

さらに、CRLFインジェクションによってレスポンスが分割されている場合、攻撃者は以下のように、本来のレスポンスとは無関係な、全く新しいレスポンスを注入できる。

HTTP/1.1 200 OK
X-User-Name:

<-- ここで改行 -->
HTTP/1.1 200 OK
Content-Type: text/html

Malicious Content

このように、レスポンス分割は、キャッシュ汚染とXSSという、Webアプリケーションにとって最も深刻な脆弱性を引き起こす可能性がある、まさに「隠れたる凶器」なんだ。

実践! CRLFインジェクションの脆弱性検出とcurlでの再現

では、実際にどうやってこの脆弱性を見つけるのか? そして、どうやって再現するのか? ここは現場のエンジニアなら、ぜひとも押さえておきたいところだ。

1. 脆弱性のあるコードの例(Python Flask)

まずは、脆弱性のあるコードのイメージを掴もう。ここではPythonのFlaskフレームワークを使った簡単な例を示す。

from flask import Flask, request, make_response

app = Flask(__name__)

@app.route(‘/greeting’)
def greeting():
# ユーザーからの入力をそのままLocationヘッダーの値として使用
# これがCRLFインジェクションの脆弱性の原因となる
user_input = request.args.get(‘name’, ‘Guest’)

# ユーザー入力をエスケープせずにそのままヘッダーに含める
response = make_response(f”Hello, {user_input}!”)
response.headers[‘X-Greeting-Name’] = user_input # ここに脆弱性がある!

return response

if __name__ == ‘__main__’:
app.run(debug=True)

このコードでは、`/greeting` エンドポイントに `name` パラメータで渡された値を、`X-Greeting-Name` ヘッダーの値としてそのままレスポンスに含めている。もし、`name` パラメータに `%0d%0a` (URLエンコードされたCRLF)や、それに続くHTTPレスポンスヘッダーを含んだ文字列を渡すと、脆弱性が顕在化する。

2. curlを使った攻撃の再現

この脆弱性を `curl` コマンドで試してみよう。攻撃者は、CRLFをURLエンコードして送信する。

本来のアクセス:

curl -v “http://localhost:5000/greeting?name=Alice”

期待されるレスポンス:

< HTTP/1.1 200 OK < Content-Type: text/html; charset=utf-8 < Content-Length: 14 < X-Greeting-Name: Alice <-- ここにAliceが入る < Server: Werkzeug/2.0.2 Python/3.9.7 < Date: Tue, 25 Oct 2023 10:00:00 GMT < Hello, Alice! CRLFインジェクションを試みる:
`%0d` は `\r`、`%0a` は `\n` を表す。攻撃者は、`name` パラメータに `Alice%0d%0aContent-Type: text/plain%0d%0a%0d%0aPwned!` のような値を渡す。

curl -v “http://localhost:5000/greeting?name=Alice%0d%0aContent-Type%3A%20text%2Fplain%0d%0a%0d%0aPwned%21”

攻撃成功時のレスポンス(一部抜粋):

< HTTP/1.1 200 OK < Content-Type: text/html; charset=utf-8 < Content-Length: 14 < X-Greeting-Name: Alice <-- ここは本来の値 < Server: Werkzeug/2.0.2 Python/3.9.7 < Date: Tue, 25 Oct 2023 10:01:00 GMT < Hello, Alice! <-- ここは本来のボディ <-- ここでレスポンスが分割されている -->
HTTP/1.1 200 OK <-- 攻撃者が注入したレスポンスヘッダー Content-Type: text/plain <-- 攻撃者が注入したContent-Type Content-Length: 7 ... Pwned! <-- 攻撃者が注入したボディ `curl` の `-v` オプションを使うと、リクエストとレスポンスの詳細なヘッダー情報が表示されるので、レスポンスがどのように分割されたかを視覚的に確認しやすい。攻撃者が注入した `Content-Type: text/plain` や、その後のボディ `Pwned!` が、本来のレスポンスの後に続いているのがわかるだろう。

3. JavaScript (Fetch API) での再現

ブラウザ環境で、Fetch APIを使って試すこともできる。ただし、ブラウザのセキュリティ機能(CORSなど)や、ブラウザがどのようにレスポンスを解釈するかによって、期待通りの結果にならない場合もある。

// 攻撃用のURL(脆弱性のあるサーバーを想定)
const vulnerableUrl = ‘http://localhost:5000/greeting’;

// 攻撃ペイロード:CRLFと、それに続く悪意のあるレスポンスヘッダー・ボディ
const maliciousPayload = ‘Alice%0d%0aContent-Type%3A%20text%2Fplain%0d%0a%0d%0a‘;

// Fetch APIでリクエストを送信
fetch(`${vulnerableUrl}?name=${maliciousPayload}`)
.then(response => {
console.log(‘Response Status:’, response.status);
// レスポンスヘッダーも確認してみる
console.log(‘Response Headers:’, response.headers.get(‘X-Greeting-Name’)); // これは正常に取得できるはず
console.log(‘Response Headers (raw):’, response.headers); // 全てのヘッダーを確認
return response.text(); // レスポンスボディを取得
})
.then(body => {
console.log(‘Response Body:’, body); // ここに攻撃者が注入した内容の一部が現れる可能性がある
// ブラウザによっては、ここでスクリプトが実行される(ただし、通常はサンドボックス化される)
})
.catch(error => {
console.error(‘Error:’, error);
});

このJavaScriptコードを実行すると、`X-Greeting-Name` ヘッダーには `Alice` が入るが、`response.headers` や `response.text()` の結果に、攻撃者が注入した内容が影響を与えている様子を確認できるだろう。ブラウザは通常、二つ目のレスポンスを別個に処理しようとするが、キャッシュ汚染や、特定の状況下でのXSSを引き起こす可能性は依然として残る。

鉄壁の防御策: CRLFインジェクションを防ぐには?

この厄介な攻撃からWebアプリケーションを守るには、どうすれば良いのか? 鍵は、「ユーザーからの入力を決して信用しない」こと、そして「出力時の厳格なエスケープ処理」にある。

1. 入力値のバリデーションとサニタイズ

まず、ユーザーから受け取った全ての入力値に対して、期待されるフォーマットや文字種を厳密にチェックする。CRLF(`\r\n`)や、それらを単独で含む文字(`\r`, `\n`)は、どのような文脈でも予期せぬ挙動を引き起こす可能性があるため、原則として拒否するか、無害な文字に置換すべきだ。

  • 拒否: `name` パラメータにCRLFが含まれていたら、リクエスト自体をエラーとする。
  • 置換: CRLFをスペースや空文字列に置換する。ただし、意図しない意味合いの変化を招く可能性もあるため、慎重な判断が必要。

2. 出力時のエスケープ処理(最も重要!)

これが最も確実で効果的な対策だ。HTTPレスポンスのヘッダーやボディにユーザー入力を出力する際には、必ず適切なエスケープ処理を施す。

  • ヘッダー値のエスケープ:

HTTPヘッダーの値にユーザー入力を含める場合、CRLF(`\r\n`)はもちろん、その他の特殊文字(`:`, `;`, `,` など)も、文脈に応じてエスケープする必要がある。多くのWebフレームワークでは、ヘッダー値を設定する際に自動的にエスケープしてくれる機能が用意されている場合がある。もし、手動でヘッダーを設定する場合は、フレームワークや言語の提供する安全なAPIを使用することを強く推奨する。

例えば、PHPであれば `header()` 関数を使う際に、`urlencode()` などでエスケープするのではなく、CRLFやそれに類する文字を正規表現で除去するなどの対応が一般的だ。

Python Flaskの例で言えば、`response.headers[‘X-Greeting-Name’] = user_input` の部分が脆弱性の原因となっている。これを安全にするには、`user_input` をエスケープする必要がある。しかし、ヘッダー値としてそのまま出力するのではなく、そもそもヘッダーにユーザー入力を直接含める設計自体を見直すのが最善だ。

  • HTMLボディのエスケープ:

HTMLボディにユーザー入力を埋め込む場合は、HTMLエンティティ(`<`、`>`、`&`、`"` など)にエスケープすることが基本となる。これにより、ブラウザは挿入された文字列をコードとして解釈するのではなく、単なるテキストとして表示するようになる。

多くのWebフレームワークは、テンプレートエンジン(Jinja2, ERBなど)を提供しており、これらはデフォルトで自動エスケープ機能を持っている。これらを活用するのが安全だ。

# Flask + Jinja2 の例
from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route(‘/profile’)
def profile():
user_name = request.args.get(‘name’, ‘Guest’)
# Jinja2はデフォルトでHTMLエスケープを行う
template = “””



Profile

Welcome, {{ user_name }}!



“””
return render_template_string(template, user_name=user_name)

if __name__ == ‘__main__’:
app.run(debug=True)

この例では、`{{ user_name }}` というJinja2の構文により、`user_name` 変数の値が自動的にHTMLエスケープされるため、XSS攻撃を防ぐことができる。

3. Web Application Firewall (WAF) の導入

アプリケーションレベルでの対策に加え、WAF(Web Application Firewall)を導入することも有効な手段だ。WAFは、HTTPリクエスト/レスポンスを監視し、既知の攻撃パターン(CRLFインジェクションを含む)を検知してブロックする。ただし、WAFは万能ではない。未知の攻撃や、WAFのルールで検知できない巧妙な攻撃に対しては、アプリケーション側の対策が最終的な防波堤となる。

4. HTTPヘッダーの適切な使用

そもそも、HTTPレスポンスヘッダーにユーザーからの入力を直接含める設計は、避けるべき場合が多い。`Location` ヘッダーであればリダイレクト先のURLとして、`Set-Cookie` ヘッダーであればCookieの値として、それぞれ期待されるフォーマットに厳密に従う必要がある。

もし、ユーザー情報をレスポンスヘッダーに含めたい場合は、その情報がどのような種類のもので、どのように使用されるのかを再考し、必要であればカスタムヘッダー(`X-` プレフィックスが付いたもの。ただし、標準化されたヘッダーの使用が推奨される)を使用するにしても、その値は安全な形式(例えば、Base64エンコードするなど)で渡すべきだろう。

まとめ: 日々の vigilance がサイバー空間の平和を守る

HTTPレスポンス分割攻撃、特にCRLFインジェクションは、一見すると地味だが、その影響は計り知れない。キャッシュ汚染による情報改ざん、XSSによるセッションハイジャックなど、Webアプリケーションの信頼性を根底から揺るがす攻撃だ。

しかし、そのメカニズムはCRLFという「改行コード」の悪用にある。そして、その対策は、ユーザー入力を決して信用せず、出力時には必ず適切なエスケープ処理を施すという、基本に忠実な姿勢にある。

Web API設計者も、インフラ運用者も、この「基本」をおろそかにしてはならない。日々、パケットの流れを監視し、コードの安全性を点検する。その日々の vigilance こそが、サイバー空間の平和を守る、最も確実な方法なんだ。

今日の話はここまでだ。何か質問はあるか? まあ、なくてもいい。この知識を胸に、君たちの現場で活かしてくれることを願っている。また会おう。

コメント

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