脆弱な「カスタムヘッダー」という幻想 —— CSRF対策の盲点と実戦的な防衛論
現場のエンジニア諸君、今日もセキュアなコードを書いているか?
「CSRF対策? うちのアプリは X-Requested-With ヘッダーを付けてるから大丈夫だ」
もし君のチームでこんな会話が聞こえてきたら、そのプロジェクトは今すぐ見直しが必要だ。なぜなら、その防御策は現代のWebブラウザの仕様と攻撃者の狡猾な手口の前では、「鍵のかかっていない玄関に『関係者以外立入禁止』と書いた張り紙を貼っている」のと同義だからだ。
今日は、カスタムヘッダーという「思い込み」がなぜ脆いのか、そして我々が現場でどう戦うべきかを論理的かつ泥臭く解説する。
—
1. なぜ「カスタムヘッダー」は防御策にならないのか
多くの開発者が誤解しているのは、XMLHttpRequest や fetch APIでカスタムヘッダーを付与すれば、ブラウザが勝手にクロスドメイン攻撃を防いでくれると思い込んでいる点だ。
確かに、ブラウザは「CORS(Cross-Origin Resource Sharing)」の仕様に基づき、プリフライトリクエスト(OPTIONSメソッド)を飛ばして「このカスタムヘッダーを付けてもいいか?」とサーバーに伺いを立てる。しかし、以下の事実を忘れていないか?
1. プリフライトは「拒否」できるが「強制」はできない: 攻撃者が送信するリクエスト自体を止めることはできない。サーバー側が「このヘッダーがないリクエストは受け付けない」というバリデーションをコードレベルで実装していない限り、攻撃は通ってしまう。
2. CORS設定のミス: 多くの開発者は、面倒くさがって Access-Control-Allow-Origin: や、不適切なドメインワイルドカードを設定する。これでは、攻撃者が細工したスクリプトが「安全なリクエスト」として認められてしまう。
3. レガシーブラウザやプラグインの穴: 特定の条件下では、ブラウザの仕様を突いてカスタムヘッダーを無視、あるいはすり抜ける手法が存在する。
2. 攻撃者が狙うPoC(概念実証)の核心
攻撃者は、ターゲットのブラウザで以下のようなスクリプトを実行させ、ユーザーのセッション(クッキー)を悪用する。
// 攻撃者が用意した罠サイトのスクリプト
fetch(‘https://victim-bank.com/api/transfer’, {
method: ‘POST’,
headers: {
‘Content-Type’: ‘application/json’,
‘X-Requested-With’: ‘XMLHttpRequest’ // サーバー側がチェックするはずのヘッダー
},
body: JSON.stringify({ amount: 1000000, to: ‘attacker’ }),
credentials: ‘include’ // セッションクッキーを勝手に付与する!
});
サーバーが「X-Requested-With があるから安心」と判断し、かつ Access-Control-Allow-Origin が攻撃者のドメインを許可している(あるいは “ になっている)場合、認証されたリクエストとして処理が実行される。 これが「カスタムヘッダーによるCSRF対策」の成れの果てだ。
—
3. 現場で「勝てる」防御の実装
カスタムヘッダーはあくまで「付加的なチェック」に過ぎない。我々が求めるのは、「改ざん不可能なトークン(Anti-CSRF Token)」と「厳格なSamesite属性」の二段構えだ。
A. バックエンド実装(Python/Flaskの例)
トークンをサーバー側で生成し、セッションに保存。リクエストごとに照合する。
from flask import Flask, request, session, abort
import secrets
app = Flask(__name__)
CSRFトークンの生成と検証
def generate_csrf_token():
if ‘_csrf_token’ not in session:
session[‘_csrf_token’] = secrets.token_hex(32)
return session[‘_csrf_token’]
@app.route(‘/transfer’, methods=[‘POST’])
def transfer():
# リクエストヘッダーからトークンを取得して比較
token = request.headers.get(‘X-CSRF-Token’)
if not token or token != session.get(‘_csrf_token’):
abort(403) # 弾く!絶対に許さない
return “成功”
B. フロントエンドの実装(JavaScript/Fetch)
HTMLのmetaタグからトークンを読み取り、すべてのリクエストにヘッダーとして付与する。
const csrfToken = document.querySelector(‘meta[name=”csrf-token”]’).content;
fetch(‘/api/transfer’, {
method: ‘POST’,
headers: {
‘X-CSRF-Token’: csrfToken, // これが本当の防御
‘Content-Type’: ‘application/json’
},
body: JSON.stringify({ … })
});
C. 最強の防具:CookieのSameSite属性
コードを書く以前に、インフラ(Webサーバー)の設定でこれを忘れるな。
Nginx設定例:
全てのセッションクッキーを厳格に制限する
add_header Set-Cookie “SameSite=Strict; Secure; HttpOnly”;
—
結びに:エンジニアの誇りとして
いいか、セキュリティ対策に「銀の弾丸」はない。カスタムヘッダーによる防御は、脆弱性診断のレポートで「対策不十分」という判定を受ける典型的な項目だ。
君たちが開発するシステムは、ユーザーの人生や企業の資産を預かっている。「これくらいで大丈夫だろう」という慢心が、最大の脆弱性を生む。
- CSRFトークンを必ず実装すること。
- Cookieには
SameSite=LaxまたはStrictを付けること。 - CORS設定は絶対にワイルドカード()を使わず、信頼できるドメインのみに限定すること。
この3つを徹底するだけで、君のシステムは攻撃者にとって「極めてコストの高い標的」に変わる。泥臭い実装こそが、最強の盾になることを忘れるな。
現場からは以上だ。コードに戻れ。
コメント