何が起きたのか
SafeStateがまとめた研究者の分析によると、攻撃はおおよそ次のように進みました:
- PolymarketのサイトはDebugBearのCDNからリアルユーザーモニタリング用のスクリプトを読み込んでいました。これはごく普通の構成です。何千ものサイトが、何十ものベンダーで同じことをしています。
- 攻撃者はそのファイルを悪意あるJavaScriptに差し替えました。ブラウザはベンダーから毎回新しく取得するため、すべての訪問者がすぐに新しいバージョンを受け取りました。
- スクリプトはPolymarketのセッションが有効かどうかを確認し、ブラウザのストレージからアカウントデータ、ウォレット情報、取引用APIの認証情報を読み取ったうえで、Polymarket自身の署名フローを使ってポジションを移したり保有資産を売却したりして、攻撃者のウォレットへ送りました。
じっくり考えるべき点があります。研究者によれば、ハードウェアウォレットも強力なパスワードも役に立ちませんでした。スクリプトは秘密鍵を一切必要としなかったからです。ユーザー自身のブラウザセッションの内側から、ログイン中のユーザーとして振る舞ったのです。
同じレポートによれば、Polymarketの修正はベンダースクリプトを削除する本番環境へのコミットでした。取引は同じ日の午後に再開されました。SafeStateは、今年このプラットフォームを襲ったベンダースクリプト絡みのインシデントはこれが初めてではないとも指摘しています。
あなたのアプリにとってなぜ重要か
cdn.some-vendor.comを指すscriptタグは、あなた自身のコードと同じ権限で動きます。DOMを読み、localStorageやsessionStorageを読み、ユーザーの認証情報であなたのAPIを呼び出し、ページの表示内容を変えられます。「自分のコード」と「ローンチ週に貼り付けたアナリティクスのスニペット」の間にサンドボックスはありません。
これは、素早く出荷されたアプリやAIで作られたアプリでは特に重要です。理由は次のとおりです:
- スクリプトは積み重なっていきます。「アナリティクスを追加して」「チャットバブルを追加して」「エラートラッキングを追加して」というプロンプトのたびに、タグが1つ増えがちです。あとで削除する人はほとんどいません。
- トークンがブラウザのストレージにあります。多くのスターターテンプレートはセッションやAPIトークンをlocalStorageに保存しており、ページ上のどのスクリプトからも読めてしまいます。
- たいていContent-Security-Policyがありません。CSPがなければ、注入されたり差し替えられたりしたスクリプトは、どこからでも追加のコードを読み込み、どこへでもデータを送れます。
ベンダーのビルドパイプラインを監査することはできません。しかし、悪意あるスクリプトが与えられる被害を制限することはできます。
アプリを確認する方法
1. ページが読み込むすべてのスクリプトを洗い出す。アプリを開いてDevToolsのNetworkタブでJSに絞り込み、リロードします。または、重要なページごとにコンソールで次を実行します:
[...document.scripts].map(s => s.src || '(inline)').filter(Boolean)自社以外のドメインをすべて書き出してください。マーケティングサイトだけでなく、ログイン後のページでも行いましょう。タグマネージャーは、HTMLソースに現れない追加のスクリプトを読み込むことがあります。
2. それらのタグのうち、integrity属性があるものを確認する。コンソールで:
[...document.querySelectorAll('script[src]')]
.filter(s => !s.src.startsWith(location.origin))
.map(s => ({ src: s.src, sri: !!s.integrity }))3. レスポンスヘッダーを確認する。curl -sI https://yourapp.com を実行し、Content-Security-Policyを探します。存在しない場合や、script-src * やnonceなしの 'unsafe-inline' を含んでいる場合は、ほとんど制限になっていません。
4. セッションの保存場所を確認する。DevToolsのApplicationでLocal StorageとSession Storageを見てください。アクセストークン、リフレッシュトークン、APIキーがあれば、ページ上のどのスクリプトからも読めます。まさにPolymarketのペイロードがしたことです。
修正方法
不要なものは削除する。これが最も効果の高いステップです。SafeStateが運営者向けに挙げる推奨事項は、サードパーティスクリプトの一覧を管理し、アカウント、決済、取引のページから必須でないタグを取り除くことから始まります。ログイン、請求、設定のページにヒートマップツールが必要なことはまずありません。
残すものはSubresource Integrity(SRI)で固定して検証する。ベンダーがバージョン付きのファイルを提供しているなら、特定のバージョンを参照してハッシュを追加します。ファイルが変わると、ブラウザは実行を拒否します:
<script
src="https://cdn.example-vendor.com/sdk/4.2.1/sdk.min.js"
integrity="sha384-REPLACE_WITH_REAL_HASH"
crossorigin="anonymous"></script>ハッシュは、自分でレビューしたファイルそのものから自分で生成してください:
curl -s https://cdn.example-vendor.com/sdk/4.2.1/sdk.min.js \
| openssl dgst -sha384 -binary | openssl base64 -Aトレードオフも理解しておきましょう。SRIは、ベンダーがバージョンなしのURLでその場で更新するスクリプトを壊してしまいます。監視やアナリティクスのスニペットではよくあることです。そうしたスクリプトについては、固定したコピーをセルフホストして意図的に更新するか、ベンダーのライブファイルを信頼していると割り切ったうえで、機密性の高いページから外してください。
Content-Security-Policyを追加する。CSPは、どのオリジンがスクリプトを配信してよいか、ページがどのオリジンへデータを送ってよいかをブラウザに伝えます。Next.jsやExpressのアプリなら、出発点は次のようになります:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example-vendor.com;
connect-src 'self' https://api.yourapp.com;
frame-ancestors 'none';
base-uri 'self';
object-src 'none'ここでCSPに何ができるかは現実的に捉えてください。ベンダー自身のドメインが許可されていれば、そこから配信される悪意あるファイルをCSPは止められません。CSPが止めるのは、そのスクリプトが新しいドメインからコードを取り込んだり、connect-srcにない任意のエンドポイントへ盗んだデータを送ったりすることです。まずContent-Security-Policy-Report-Onlyで展開し、何が壊れるかを確認しましょう。Content-Security-Policyの追加についてのガイドで、手順を一つずつ解説しています。
セッショントークンをJavaScriptの手の届かない場所へ移す。セッショントークンはHttpOnly、Secure、SameSiteを付けたCookieに移し、ページのスクリプトから読めないようにします。ページが開いている間に悪意あるスクリプトがユーザーとしてリクエストを送ることは防げませんが、トークンを盗まれて別の場所で再利用されることは防げます。Secure・HttpOnly・SameSiteのCookieフラグを参照してください。
自分のフロントエンドのキーをきれいに保つ。スクリプトがバンドルを読めるなら、そこに含めて出荷したシークレットもすべて読めます。そしてボットはすでに漏れたキーや.envファイルを探し回っています。クライアント側JavaScriptで公開されたAPIキーのガイドを確認し、本番環境のソースマップを配信していないか確かめてください。すでにヒヤリとした経験があるなら、ダウンタイムなしでアプリのシークレットをローテーションする方法の記事が後始末に役立ちます。
まとめのチェックリスト
DevToolsを閉じる前に、次のことを確認しましょう:
- 公開ページとログイン後のページにあるサードパーティスクリプトをすべて洗い出した。
- 使っていないスクリプトを、特にログイン、請求、アカウントのページから削除した。
- ベンダーが対応している場合は、バージョンを固定してintegrityハッシュを追加した。
- 固定できないスクリプトはセルフホストするか、機密性の高いページから外した。
- script-srcとconnect-srcを厳しく絞ったContent-Security-Policyを導入した。
- セッショントークンをlocalStorageではなくHttpOnly Cookieに保存している。
- 誰か(あるいはAIエージェント)が新しいタグを追加するたびに、この監査をやり直す。
現状を把握しよう
手早く基準を知りたいなら、AppSafeはContent-Security-Policyが設定されているかを含めてセキュリティヘッダーをチェックし、所有権を確認したフルスキャンではSubresource Integrityや脆弱なJavaScriptライブラリといったフロントエンドの問題も調べます。レポートの見た目はサンプルレポートで確認できます。AIで作ったアプリ向けのローンチ前チェックリストとあわせて活用するのもおすすめです。
Polymarketは取引を停止してから約1時間で原因を突き止めました。多くの小規模チームは、スクリプトの変更に気づくまでにもっと長くかかるでしょう。今日、AppSafeの無料スキャンを実行して、自分のアプリの現状を確かめてください。
