5/13 ローンチ予定!
PiloTube

PiloTube 開発日誌

← 「ひとり社長のAI開発記」一覧へ

200が返ってるのになぜか動かない、CSPの罠

約7分で読めます

Vercel CSPでreCAPTCHAスクリプトがブロックされていた話

ステータスは200。エラーログもない。なのにreCAPTCHAが動かない。

この状況、最初は完全に意味がわからなかった。「signup-verify のエンドポイントは正常に返ってるのに、なぜ?」と30分ほど頭を抱えた。答えはシンプルで、しかも「あ、そういうことか」と苦笑いするしかない見落としだった。


問題:「動いてる」と思っていた

PiloTube(パイロチューブ)の会員登録フローに reCAPTCHA v3 を導入した。スパム登録対策として必要な機能で、実装自体はそこまで複雑じゃない。バックエンドのエンドポイントを叩いてトークンを検証する部分は一通り書いて、ローカルで確認したら問題なく動いた。

「よし、本番に上げよう」

Vercel にデプロイして、ブラウザから signup フローを動かしてみる。エンドポイントは200を返している。バックエンドのログにもエラーはない。「完成だ」と思ってブラウザのコンソールを閉じようとしたとき、赤いエラーが目に入った。

Refused to load the script 'https://www.google.com/recaptcha/api.js'
because it violates the following Content Security Policy directive:
"script-src 'self' ..."

reCAPTCHAのスクリプト自体が読み込まれていなかった。


気づき:CSPが全部止めていた

Content Security Policy、略してCSP。「どのオリジン(ドメイン)からのリソースを許可するか」をHTTPヘッダーで制御する仕組みだ。XSS(クロスサイトスクリプティング)対策として非常に有効で、PiloTubeの本番環境では Vercel の設定でCSPを有効にしていた。

問題は、script-src の許可リストに www.google.com を追加していなかったこと。

reCAPTCHA v3 は https://www.google.com/recaptcha/api.js を外部スクリプトとして読み込む。CSPが 'self'(自分のドメインのみ)しか許可していなければ、当然ブロックされる。

ローカル開発環境ではCSPを厳密に設定していなかったので、この問題は出なかった。本番にデプロイして初めて発覚する類の罠だった。


課題:「なぜ200が返ってくるのか」で混乱した

正直、最初に混乱したのはここだった。

「signup-verify のエンドポイントが200を返している」という事実が、問題の特定を遅らせた。バックエンドは正常に動いている。なのにreCAPTCHAが機能しない。「バックエンドの実装に何か問題があるのか?」「トークンの渡し方が間違っているのか?」と、엉뚱한方向で考え始めていた。

実際の流れを整理すると、こうなる。

  1. フロントエンドが www.google.com/recaptcha/api.js を読み込もうとする
  2. CSPがブロックする → スクリプトが読み込まれない
  3. reCAPTCHAトークンが生成されない
  4. トークンなしでフォームが送信される
  5. バックエンドは「トークンなし」の状態を受け取って処理する → 200を返す

つまりバックエンドは「reCAPTCHAなし」の状態でリクエストを処理していただけで、reCAPTCHAの検証自体が走っていなかった。200が返ってきていたのは「正常動作」ではなく「reCAPTCHAをスルーした状態」だった。

これは普通にまずい。スパム登録が素通りできる状態だった。


実践:Vercel の vercel.json を修正する

修正自体は5分で終わった。vercel.json の CSP ヘッダー設定に www.google.com を追加するだけだ。

修正前の script-src はこんな感じだった(簡略化):

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy",
          "value": "script-src 'self' 'nonce-xxx';"
        }
      ]
    }
  ]
}

これを以下のように修正した:

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy",
          "value": "script-src 'self' 'nonce-xxx' https://www.google.com https://www.gstatic.com;"
        }
      ]
    }
  ]
}

www.google.com だけでなく www.gstatic.com も追加している。reCAPTCHA v3 は内部的に gstatic.com からもリソースを読み込むため、これも許可しないと別のCSP違反が発生する。最初は www.google.com だけ追加してデプロイしたら、今度は gstatic.com のエラーが出た。2回デプロイする羽目になった。


結果:ブラウザコンソールがきれいになった

修正後にデプロイして、ブラウザのコンソールを開きながら signup フローを動かした。

赤いエラーが消えた。reCAPTCHAのバッジ(画面右下に出る小さなGoogle表記)も表示されるようになった。トークンが正常に生成されて、バックエンドでの検証も通るようになった。

「ようやくちゃんと動いた」という感覚があった。200が返ってくることと「正しく動いている」ことは、別の話だと改めて思い知らされた瞬間だった。


教訓:本番テストは「コンソールを開いた状態で」やる

この件で学んだことをまとめる。

1. ステータスコードだけ見ていると見逃す

200が返ってきていても、フロントエンドで何かがブロックされていれば「正しく動いている」とは言えない。本番テストのときはブラウザの開発者ツール(コンソール・ネットワークタブ)を必ず開いた状態で確認する習慣をつける。

2. 外部スクリプトを使うときはCSPを先に確認する

reCAPTCHA、Stripe、Google Analytics、Intercom……外部サービスのスクリプトを組み込むときは、必ずそのサービスが使うドメインをCSPに追加する必要がある。各サービスのドキュメントに「CSPの設定」セクションがあることが多いので、実装前に確認するのが正解だった。

reCAPTCHA v3 の場合、必要なドメインはこの2つ:

  • https://www.google.com
  • https://www.gstatic.com

3. ローカルと本番のCSP設定を揃えておく

開発環境でCSPを緩くしていると、本番デプロイ後に初めて問題が発覚する。できれば開発環境でも本番に近いCSP設定で動かしておくか、少なくとも「本番には厳しいCSPがある」と意識しながら外部リソースを追加する。

4. 「動いてるっぽい」で終わらせない

これが一番の反省だ。ローカルで動いたことを確認して「完成」と判断してしまった。本番環境で一通りのフローをコンソール付きで確認するステップを省いてしまったのが、この問題を見逃した根本的な原因だった。


CSPは「設定しておくと安心」なセキュリティ機能だけど、外部サービスを追加するたびに設定の更新が必要になる。地味で見落としやすい作業だが、こういうところで穴が開く。PiloTubeはまだ開発中の機能が多いので、外部サービスを組み込むたびにCSPの確認をルーティン化していくつもりだ。

チャプター生成AI

URL貼るだけ。AIがチャプターを自動生成。

1

YouTubeのURLをコピーして貼る

2

「生成する」を押す

3

概要欄にコピペして完了

無料でチャプターを生成する →

月3回まで無料 · クレジットカード不要