JSONPの「罠」:コールバック関数名という盲点を突くXSS攻撃の正体
現場でインシデント対応をしていると、「なぜ今さらJSONPなのか?」という声を聞くことがある。確かにモダンな開発現場ではCORS(Cross-Origin Resource Sharing)が標準だが、レガシーなシステムとの連携や、特定の広告配信タグ、あるいは古い社内ツールにおいて、JSONPは今もなお「死に体のゾンビ」のように生き残っている。
JSONP(JSON with Padding)は、クロスドメイン制約を回避するために「サーバー側でクライアント指定の関数名でJSONをラップして返す」という手法だ。しかし、この「クライアント指定」という仕様そのものが、セキュリティ屋から見れば最大の脆弱性になり得る。
今日は、開発者が「まさかここが?」と見落としがちな、JSONPにおけるコールバック関数のエスケープ漏れについて、その実態と防御策を叩き込む。
—
なぜコールバック関数が攻撃対象になるのか
JSONPの基本的な挙動はこうだ。
1. クライアント:GET /api/data?callback=processData
2. サーバー:processData({"id": 1, "name": "user"})
ここで攻撃者は、callback パラメーターに悪意あるスクリプトを仕込む。
GET /api/data?callback=alert(document.cookie)//
サーバー側でこの値をそのまま出力すれば、レスポンスは以下のようになる。
alert(document.cookie)//({"id": 1, "name": "user"})
見事にJavaScriptが実行される。これがJSONPにおけるXSSのメカニズムだ。ブラウザはこれを「正しいJSスクリプト」と認識し、迷わず実行する。防壁となるべきContent-Typeヘッダーも、タグ経由で読み込まれるJSONPには無力だ。
---
「ただの文字列」として扱うな:セキュアな実装パターン
「コールバック関数名に記号が含まれていたらエラーにする」というのが鉄則だが、これを正規表現でこねくり回すのはバグの元だ。もっとシンプルで堅牢な、ホワイトリスト方式を採用すべきだ。
PHPでの実装例(厳格なバリデーション)
'Invalid callback name']);
exit;
}
// 2. データ生成
$data = ['status' => 'success', 'data' => 'secret_info'];
$json = json_encode($data);
// 3. レスポンス送信(Content-Typeはapplication/javascriptに設定)
header('Content-Type: application/javascript; charset=utf-8');
echo "{$callback}({$json});";
Python (Flask) での実装例
from flask import Flask, request, jsonify, Response
import re
app = Flask(__name__)
@app.route('/api/data')
def get_data():
callback = request.args.get('callback', 'callback')
# 許可リスト以外の文字が含まれていたら拒否
if not re.match(r'^[a-zA-Z0-9_]+$', callback):
return jsonify({"error": "Invalid callback"}), 400
data = {"id": 123, "val": "secure_data"}
response_text = f"{callback}({jsonify(data).get_data(as_text=True)});"
return Response(response_text, mimetype='application/javascript')
---
インフラ層での防御:WAFとヘッダーの活用
コード修正が間に合わない場合や、多層防御の観点からは、WAFやHTTPヘッダーで蓋をする必要がある。
Content-Typeヘッダーの強制
JSONPエンドポイントは必ず application/javascript を送出すること。text/html としてブラウザが誤解釈するリスクを排除する。
CSP(Content Security Policy)
もし可能なら、CSPの script-src を厳格に設定し、信頼できないドメインからのスクリプト実行を禁止する。ただし、JSONP自体がインラインスクリプトのように動くため、JSONPを利用している限りCSPでの防護は難しい。これが「JSONPを廃止すべき最大の理由」でもある。
NginxでのWAF設定(ModSecurityの例)
もしModSecurityを導入しているなら、コールバックパラメーターに怪しい記号が含まれていないかチェックするルールを書く。
ModSecurityのルール例(簡易版)
SecRule ARGS:callback "!^[a-zA-Z0-9_]+$" \
"id:1001,phase:1,deny,status:400,msg:'Invalid JSONP callback name detected'"
---
現場のエンジニアへ:明日からやるべきこと
1. JSONPの全廃を目指す: まだJSONPを使っている箇所をリストアップせよ。そして、可能な限り CORS に置き換える計画を立てろ。
2. バリデーションは「ホワイトリスト」のみ: htmlspecialchars や json_encode でエスケープすれば大丈夫、という甘い考えは捨てろ。コールバック関数名は、許可された文字セット以外は徹底的に拒否する。
3. 点検: callback というパラメーター名以外に、cb, jsonp, callback_func など、別名で隠されていないかgrepで全検索せよ。
JSONPの脆弱性は、攻撃者からすれば「玄関の鍵を自分で開けっ放しにしている」ようなものだ。技術的な負債は放置すればするほど、後のインシデント対応で自分の首を絞めることになる。今すぐ、そのエンドポイントを覗いてみてほしい。君のコードは、本当に「名前」を正しく扱っているだろうか?
コメント