なぜ「WebAuthnのOrigin検証」で手を抜くのか?――FIDO2実装の死角を突く攻撃と防御の鉄則
現場でエンジニア諸君が「WebAuthnを実装した」と胸を張る際、私がまず確認するのはライブラリの呼び出し方ではない。「サーバー側のRP ID検証ロジック」だ。
多くの開発者がライブラリの便利機能に甘え、Origin検証を疎かにして「なぜか動くから」で済ませている。だが、WebAuthnにおけるOrigin検証は、ただの「おまじない」ではない。これこそが、フィッシングサイトによる認証情報の盗用を物理的に遮断するための、最後の防波堤なのだ。
1. 攻撃者が狙う「Originの隙間」
WebAuthnにおける最も恐ろしい攻撃は、中間者攻撃(AiTM: Adversary-in-the-Middle)だ。
攻撃者はプロキシサーバーを立て、ユーザーを偽のドメイン(例: login-bank-secure.com)へ誘導する。ユーザーがそこで認証を通すと、ブラウザは「現在のページ(偽ドメイン)」のOriginをWebAuthnの認証データに署名として埋め込む。
もし、あなたがサーバー側で以下を怠っていたらどうなるか。
1. RP ID(Relying Party ID)の静的検証の欠如
2. ブラウザが送信してきたoriginプロパティの比較漏れ
攻撃者は、盗み取った認証データ(Assertion)を、あなたの本物のドメイン(bank.com)に対してリプレイする。サーバー側でOriginを厳格にチェックしていなければ、あなたのシステムは「ああ、正しい署名だね」と攻撃者に認証を通してしまう。これが、パスワードレス認証の最前線で起きている現実だ。
2. 実装の鉄則:サーバー側で何を検証すべきか
ブラウザ(クライアント)は、WebAuthn APIを実行する際に必ずOriginを確認するが、サーバー側で再検証しないのは「鍵をかけずに玄関に泥棒を入れる」のと同義だ。
サーバー側で実施すべきは以下の2点である。
1. rpId の一致: サーバーが期待するドメイン(example.com)と、アサーションに含まれるrpIdHashが一致しているか。
2. origin の検証: ブラウザから送られてきたオリジンが、ホワイトリストに登録されている値と完全一致するか。
3. 実践:Python (Flask/FIDO2ライブラリ) での堅牢な検証
世の中には便利なライブラリがあるが、検証ロジックをラップしすぎて中身が見えなくなるのが一番怖い。ここでは fido2 ライブラリを使用した、最低限かつ必須の検証コードを示す。
from fido2.server import Fido2Server
from fido2.webauthn import AuthenticatorData
サーバーの設定
開発環境と本番環境で確実に分けること
EXPECTED_ORIGIN = “https://your-secure-app.com”
RP_ID = “your-secure-app.com”
def verify_assertion(credential_id, client_data, auth_data):
“””
クライアントから送られてきたassertionを検証する
“””
# 1. Originの検証 (最優先)
# ブラウザが提供したclient_dataのoriginを直接比較する
if client_data.origin != EXPECTED_ORIGIN:
raise ValueError(“不正なOriginからの認証要求を検知しました”)
# 2. RP IDの検証
# ライブラリ側の検証機能に依存しつつ、RP IDが期待するものか確認
# FIDO2サーバーの設定でRP_IDをハードコードしておく
server = Fido2Server({“id”: RP_ID, “name”: “Secure App”})
# 署名とIDの検証を実行
# ここで内部的に auth_data の rp_id_hash と RP_ID が比較される
server.verify_assertion(
credential_id,
client_data,
auth_data,
# …必要な認証情報の取得処理…
)
return True
4. インフラ側の防壁:Nginx/WAFでの制約
コードだけでは不十分だ。インフラレイヤーでも「許容しないOrigin」を弾く準備をしておくべきだ。特に開発環境から本番環境への意図しない跨ぎを防ぐため、Access-Control-Allow-Origin ヘッダーの動的生成には細心の注意を払え。
Nginx設定: 厳格なドメイン指定
map $http_origin $cors_origin {
default “”;
“https://your-secure-app.com” “https://your-secure-app.com”;
}
server {
server_name your-secure-app.com;
location /api/auth {
# 不正なオリジンを問答無用で拒否する設定
if ($cors_origin = “”) {
return 403;
}
add_header ‘Access-Control-Allow-Origin’ $cors_origin;
add_header ‘Access-Control-Allow-Credentials’ ‘true’;
# …以下プロキシ設定…
}
}
最後に:セキュリティは「性悪説」で構築せよ
WebAuthnは魔法ではない。実装者が「ブラウザがやってくれるだろう」「ライブラリがやってくれるだろう」という期待という名の怠慢を抱いた瞬間、その脆弱性は攻撃者の格好の標的になる。
- Originは常にサーバー側で文字列比較せよ。
- RP IDは環境変数で厳格に管理せよ。
- 「動く」ことと「安全である」ことは別物だと肝に銘じよ。
コードを書くとき、常に「これが攻撃者の手元でどう悪用されるか?」と自問自答してほしい。その一瞬の疑念が、明日のインシデントを防ぐ唯一の武器になるのだから。
健闘を祈る。何かあればいつでも聞け。
コメント