なぜ「WAFが通したリクエスト」が、バックエンドで牙を剥くのか?――HTTPパラメータ汚染(HPP)の現実
現場でセキュリティ診断をしていると、必ずと言っていいほど「WAFを入れているからうちは大丈夫です」というエンジニアに出会う。だが、はっきり言おう。WAFは万能の盾ではない。
今日のテーマは、WAFとバックエンドサーバーの「解釈のズレ」を突く、HTTPパラメータ汚染(HPP: HTTP Parameter Pollution)だ。これは教科書的なインジェクション攻撃とは異なり、サーバー製品やミドルウェアの「仕様の差」という、もっと泥臭いレイヤーの脆弱性だ。
1. HPPが成立するメカニズム:解釈の「ゆらぎ」
HPPの正体は、同じ名前のパラメータが複数送られてきた時の処理ルールが、フロント(WAF/リバースプロキシ)とバックエンド(Webアプリケーション)で異なることにある。
例えば、攻撃者が以下のようなURLを送りつけたとしよう。
GET /search?id=1&id=2
この時、各サーバーがどう判断するか。
- WAFの解釈: 最初に出現した
id=1だけを検証する(あるいは、連結して1,2と解釈する)。 - バックエンドの解釈: 最後に出現した
id=2を優先する(または、配列として展開する)。
もしWAFがid=1(正常値)を検査して「パス」を出しても、バックエンド側でid=2(悪意のある値)が採用されれば、セキュリティフィルタは無力化される。これがHPPの恐ろしさだ。
2. 攻撃の構図:WAF回避のPoC
SQLインジェクションを例に挙げよう。バックエンドのクエリが SELECT FROM users WHERE id = $_GET['id'] だとする。
攻撃者は ?id=1&id=1 UNION SELECT ... と送る。
WAFは id=1 を見て「問題なし」と判断するが、バックエンドが後ろの値を採用すれば、そのままSQLiが成立する。
3. 実践的な防御:コードレベルでの対策
「WAF任せ」を捨て、アプリコード側で「パラメータの重複」を厳格に制御することが、この攻撃を無力化する唯一の解法だ。
Python (Flask) でのセキュアな実装例
Flaskでは request.args.get('id') と書くと、デフォルトで最初の値が取得されるが、これではHPPのリスクを排除できない。「重複したパラメータはそもそも受け付けない」という設計思想が必要だ。
from flask import Flask, request, abort
app = Flask(__name__)
@app.route(‘/search’)
def search():
# request.args.lists() を使うと全ての値をリストで取得できる
params = request.args.lists()
# パラメータに重複がある場合は即座に拒否する(これが最も安全な実装)
if any(len(values) > 1 for key, values in params):
abort(400, description=”不正なパラメータ重複が検出されました”)
user_id = request.args.get(‘id’)
# 以下、バリデーション済みのIDのみを処理する
# …
PHP でのセキュアな実装例
PHPの $_GET は、デフォルトでは「最後に来たパラメータ」を上書きする仕様だ。これを防ぐには、入力の整合性をチェックするラッパー関数を挟む必要がある。
function getValidatedParam($key) {
// 重複したパラメータが渡された場合、$_GET は最後の一つしか保持しない。
// そのため、$_SERVER[‘QUERY_STRING’] をパースして重複チェックを行う。
parse_str($_SERVER[‘QUERY_STRING’], $allParams);
// 配列になっている(重複がある)場合はエラーにする
if (is_array($allParams[$key])) {
throw new Exception(“セキュリティ警告: パラメータの重複を検知しました”);
}
return $_GET[$key] ?? null;
}
4. インフラ側(Nginx / WAF)での防御設定
コード修正が間に合わない場合や、多層防御としてインフラ側でもケアが必要だ。Nginxをリバースプロキシとして使用している場合、パラメータの重複を許可しない設計にすることも検討すべきだ。
Nginxの設定例:
Nginx自体には「重複を禁止する」直接的なディレクティブはないが、Luaモジュール(OpenResty)を使用すれば、リクエストの全パラメータを検査できる。
OpenRestyでの簡易的な重複チェック例
location / {
access_by_lua_block {
local args = ngx.req.get_uri_args()
for k, v in pairs(args) do
if type(v) == “table” then
ngx.log(ngx.ERR, “HPP attempt detected: ” .. k)
ngx.exit(400)
end
end
}
proxy_pass http://backend_server;
}
チーフエンジニアからの教訓
HPPは、「システム全体で同じデータを見ていない」という設計の甘さを突く攻撃だ。
1. 正規化の統一: WAFとバックエンドでパラメータの解釈順序やエンコーディングを一致させること。
2. 重複の禁止: 業務上、同じパラメータを複数送る必要がない限り、重複パラメータはエラーとするのが開発の鉄則だ。
3. WAFはあくまで「予防接種」: 脆弱性そのものを治すのはアプリのコードだ。WAFを「全自動の全能神」と勘違いしているチームからは、必ずと言っていいほどインシデントが発生する。
現場のコードを見てほしい。request.get('id') とだけ書かれたその行が、明日、あなたのシステムを崩壊させる爆弾になっているかもしれない。今日、リポジトリを確認しよう。それが最初の一歩だ。
コメント