【実務・中級編】 多要素認証(MFA)のフィッシング耐性強化とFIDO2/WebAuthnの導入 – ガバナンス・リスク管理 & 生成AIセキュリティ防御ガイド

その認証、まだ「穴」だらけだ。FIDO2/WebAuthnでフィッシングを過去の遺物にする方法

エンジニア諸君、日々お疲れ様。今日もコードを書き、サーバーを守り、頭を悩ませていることだろう。

さて、読者の中には「うちはTOTP(6桁の数字を入力するやつ)を導入しているから安全だ」と胸を張っている者がいるかもしれない。だが、断言しよう。TOTPやSMS認証は、現代のフィッシング攻撃の前では無力に等しい。 攻撃者がプロキシサーバー(Evilginx等)を立ててリアルタイムでトークンを中継すれば、いとも簡単に突破される。

今日は、そんな「古臭いセキュリティ」に終止符を打ち、公開鍵暗号方式による最強の防壁、FIDO2/WebAuthnの実装について解説する。

—

なぜ「フィッシング耐性」が必要なのか(PoCのリスク)

攻撃者は、巧妙に作り込まれた偽サイトへユーザーを誘導し、inputタグに打ち込まれたワンタイムパスワードを背後でリアルタイムに拾い上げ、本物のサイトへ瞬時に転送する。これが「中間者攻撃(AiTM)」だ。

TOTPは単なる「パスワードの代わり」であって、「誰に対して認証しているか」というOrigin(ドメイン)の検証機能を持っていない。 だから、偽サイトに情報を渡しても、システムは「正しい数字が来た」と判断してしまう。

対してFIDO2は、ブラウザがOriginを検証し、ハードウェアキーがそのドメインと結びついた署名を作成する。偽サイトでは署名が一致せず、認証が成立しない。これが「フィッシング耐性」の正体だ。

—

WebAuthnの実装:バックエンドのロジック

WebAuthnは複雑そうに見えるが、要は「チャレンジ」を生成し、ブラウザ経由で鍵に署名させ、その結果を検証するだけだ。今回はPython(Flask)を例に、サーバーサイドの要所を解説する。

サーバーサイド:登録時のチャレンジ生成

# サーバー側で生成するチャレンジ。必ずセッションに保持し、使い回しを防ぐ
import os
import base64

def generate_registration_options(user_id, username):
    # チャレンジは暗号論的に安全な乱数であること
    challenge = os.urandom(32)
    
    # クライアント(ブラウザ)に送るオプション
    options = {
        "challenge": base64.b64encode(challenge).decode('utf-8'),
        "rp": {"name": "My Secure Service", "id": "example.com"},
        "user": {
            "id": base64.b64encode(user_id).decode('utf-8'),
            "name": username,
            "displayName": username
        },
        "pubKeyCredParams": [{"alg": -7, "type": "public-key"}] # ES256を使用
    }
    return options

クライアントサイド:ブラウザでの認証実行

フロントエンドでは navigator.credentials.create を使用する。ここでは PublicKeyCredential を呼び出すための最短のJSコードを示す。

// サーバーから受け取ったoptionsを引数に実行
async function registerCredential(options) {
    // サーバーから来たbase64文字列をArrayBufferに変換する関数が別途必要
    options.challenge = Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0));
    options.user.id = Uint8Array.from(atob(options.user.id), c => c.charCodeAt(0));

    try {
        const credential = await navigator.credentials.create({
            publicKey: options
        });
        console.log("登録成功:", credential);
        // このcredentialをサーバーへ送信し、検証を行う
    } catch (err) {
        console.error("認証失敗:", err);
    }
}

—

運用上の「泥臭い」鉄則

コードが書けただけでは、セキュリティは完成しない。現場でインシデントを起こさないために、以下の3点を徹底してくれ。

1. Originの厳格な検証

rpId(Relying Party ID)は、デプロイするドメインと完全に一致させること。サブドメイン間で共有する場合も、スコープを最小限に絞る。ここが甘いと、ドメイン検証というFIDO2の最大のメリットが死ぬ。

2. バックアップ認証の罠

ハードウェアキーを紛失した際、多くのシステムが「メールによるリセット」を許可する。ここが最大の脆弱性だ。

  • リカバリコードを物理的に印刷させる。
  • 信頼できるデバイスを複数登録させる。
  • 紛失時のリカバリプロセスそのものを、最も高い認証レベル(例:管理者による対面確認)に設定する。

3. WAFでの保護(Nginx設定例)

FIDO2のエンドポイントは、ボットによる総当たり攻撃の対象になりやすい。WAF(ModSecurityやNginxのRate Limit)で保護をかけるのは鉄則だ。

# Nginxで認証エンドポイントへのリクエストを制限
location /api/auth/webauthn {
    limit_req zone=auth_limit burst=5 nodelay;
    # 信頼できるOrigin以外からのアクセスを拒否
    if ($http_origin !~* "^https://(www\.)?example\.com$") {
        return 403;
    }
    proxy_pass http://backend_upstream;
}

—

最後に:セキュリティは「諦めない」ことの積み重ねだ

「実装が面倒だ」「ユーザーにハードウェアキーを買わせる予算がない」……そんな言い訳を耳にすることもある。だが、一度でも大規模なフィッシング被害に見舞われれば、その代償はハードウェアキーの調達コストなど比較にならないほど大きい。

インシデントハンドリングの現場でいつも思うのは、「攻撃者は最も楽な道を通り、守る側は最も面倒な道を通らなければならない」という非対称性の厳しさだ。しかし、WebAuthnはその非対称性を、強力な暗号学でこちら側に有利に傾けてくれる。

まずは開発環境で WebAuthn を触ってみてほしい。そして、自分たちのサービスを、攻撃者が「狙う気すら失せる」ほど堅牢なものに変えていこう。質問があれば、いつでも現場の知見を共有する。次もまた、泥臭く、かつスマートな防御技術の話をしよう。

コメント

タイトルとURLをコピーしました