目次
はじめに
供給チェーンシリーズのcooldown・Hardened Mode・CodeQLはすべてnpmレジストリ経由で入ってくる依存関係に対する防御でした。しかし現代のWebアプリが外部コードを取り込む経路はnpmだけではありません。
<script src="https://cdn.example.com/widget.js"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/...">
この1行の<script>または<link>はnpmゲートを完全に迂回します。そしてこの経路を通じて発生した2024年の大型インシデントがPolyfill.io事件です。この記事では、そのインシデントとブラウザレベルの防御メカニズムである**Subresource Integrity(SRI)**を解説します。
どんなリスクがあるか: CDNは信頼の単一障害点
<script src="https://cdn.example.com/widget.js">をページに追加した瞬間、次の仮定に依存することになります。
cdn.example.comドメインが私たちが信頼する運営者の管理下にある- その運営者のインフラが侵害されない
- その運営者が意図的に悪意あるコードを挿入しない
この3つの仮定のうち1つでも崩れれば、私たちのページを訪問するすべてのユーザーのブラウザで外部制御のコードが実行されます。そして崩れる事例は意外と多いです。
- ドメインの売却・買収: 元の運営者がドメインを売却し、新しい所有者が悪意を持って運営
- DNS/CDNの侵害: ドメインはそのままだが応答するコンテンツが改ざんされる
- メンテナーのサボタージュ: 意図的に悪意あるコードを挿入
- CDNインフラの侵害: 運営者も知らない間にコンテンツが改ざんされる
最も怖い点は、npmとは違ってCDNのレスポンスはリクエストのたびに取得するため、cooldownのような時間ベースの防御が通用しないことです。改ざんされた瞬間からすべてのユーザーが即座に影響を受けます。
実際の事例: Polyfill.io(2024年)
Polyfill.ioはかつて事実上の標準polyfill CDNでした。100,000以上のWebサイトが<script src="https://cdn.polyfill.io/v3/polyfill.min.js">の1行でこのサービスを使用していました。
インシデントの経緯は次のとおりです。
- 2024年2月: 中国企業のFunnullが
polyfill.ioドメインとGitHubアカウントを買収 - 2024年6月24日: Sansecがcdn.polyfill.ioがユーザーのモバイルデバイスに悪意あるコードを挿入していることを公開
- 悪意あるコードは選択的に動作 — 一部のデバイス・時間帯・管理者ページでは潜伏し、一般ユーザーにのみ有効化(検出回避)
- 影響サイトは100,000以上 — JSTOR、Intuit、Booking.comなどを含む
- Cloudflareはcdn.polyfill.ioをリアルタイムで自社の安全なバージョンにリライト、Namecheapはドメインを保留処理
このインシデントの本質はシンプルです。
運営者が変わったが、
<script>の1行はそのままだった。
コード1行も変更していないサイトが一斉に感染しました。このインシデントが示したのは「外部ホストの信頼性は時間の経過とともに変化しうるし、私たちはそれを自動的に検知できない」という点です。
Subresource Integrityの動作方式
Subresource Integrity(SRI)はこの問題をブラウザレベルで解決するW3C標準です。
原理はシンプルです。<script>または<link>タグにコンテンツのハッシュ値を一緒に記述しておくと、ブラウザはダウンロードしたコンテンツのハッシュが一致するときのみ実行・適用します。
<script
src="https://cdn.example.com/widget.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>
integrity: 正規コンテンツのハッシュ(sha256/sha384/sha512)crossorigin: SRI検証に必要なCORSモード
この状態でcdn.example.comが改ざんされて別のコンテンツを返しても、ブラウザはハッシュの不一致を検知してスクリプトの実行を拒否します。コンソールにセキュリティエラーが出力され、そのスクリプトのコードは一切実行されません。
Polyfill.ioのインシデント時点でSRIが適用されていたサイトはどうだったでしょうか? ハッシュが改ざんされたコンテンツと合わなかったため自動でブロックされました。サイトがpolyfill機能を失うだけで、ユーザーデータは安全です。
どのように適用するか
1. ハッシュの生成
次のようにハッシュを計算できます。
curl -s https://cdn.example.com/widget.js \
| openssl dgst -sha384 -binary \
| openssl base64 -A
またはオンラインツールとしてsrihash.orgのようなサイトを利用することもできます。
2. HTMLタグに適用
- <script src="https://cdn.example.com/widget.js"></script>
+ <script
+ src="https://cdn.example.com/widget.js"
+ integrity="sha384-<ハッシュ>"
+ crossorigin="anonymous"
+ ></script>
3. Vite/webpackプラグインで自動化
バンドラーを通じて外部アセットが決定される場合、SRIを自動挿入するプラグインがあります。
- Vite:
vite-plugin-sri3 - webpack:
webpack-subresource-integrity
こうしたプラグインを使えばビルド時に自動でハッシュが計算されてHTMLに挿入されます。外部CDNのアセットもビルド時に一度ダウンロードしてハッシュを計算できます。
Google FontsのようにSRIを使えない場合
Google FontsのCSSはSRIを適用するのが難しいです。その理由は:
Google Fonts CSSはリクエストしたブラウザのUser-Agentに応じて動的に異なるコンテンツを返します。同じURLでもChromeとSafariに異なる
@font-faceルールが返されます。
ハッシュは固定値でなければならないのに、レスポンスが毎回異なるため、SRIを適用しても毎回検証が失敗します。
代替案: フォントをセルフホスティング
最もすっきりした解決策はフォントをバンドルに含めることです。
yarn add @fontsource/noto-sans-jp
// main.tsx
import '@fontsource/noto-sans-jp/400.css'
import '@fontsource/noto-sans-jp/700.css'
こうすることで次のメリットがすべて得られます。
- SRIを適用する必要自体がなくなる(外部ホストがなくなる)
- ユーザーのIPがGoogleに送信されるGDPRの問題が解決
- フォントの読み込み速度が向上(CDN ↔ 自ドメイン間のハンドシェイクが不要に)
- オフライン環境でも動作
同じパターンが@fontsource/inter、@fontsource/robotoなどほぼすべてのGoogle Fontsについてnpmパッケージとして提供されています。
限界
- SRIはコンテンツの変更自体を防ぎません。コンテンツが変更されるとブラウザはそれを実行しないだけで、正規コンテンツも一緒に動作を止めます。つまり、可用性を犠牲にして完全性を確保するトレードオフです。
- バージョン更新のたびにハッシュの更新が必要。外部アセットが更新されればハッシュも更新しなければなりません。ビルドの自動化がなければ運用の負担になります。
- 動的コンテンツには適用不可。Google Fonts CSS、Tag Manager、広告SDKなど毎回異なるレスポンスを返すアセットにはSRIを適用できません。この場合はセルフホスティングが唯一の答えです。
- CDN自体のセキュリティインシデントは防ぎますが、CDNと一緒に紐づいている他の信頼の仮定(例: ドメインを通じたCookieの露出)は別途対処が必要です。
まとめ
SRIは1行の変更で最も大きなカテゴリの外部コード攻撃(CDNの改ざん)を1つ無力化する、非常にROIの高い防御です。Polyfill.ioのインシデントでSRIを適用していたサイトとそうでないサイトが受けた被害の差は決定的でした。
推奨ステップ:
- 現在外部からロードしているすべてのアセットを調査する —
<script src="https://...">、<link href="https://..."> - 固定コンテンツにはSRIハッシュを追加する — ビルド時に自動化可能
- 動的コンテンツ(Google Fontsなど)はセルフホスティングに切り替える — fontsource・自前の静的ホスティング
- バンドラープラグインで自動化する — 外部アセットが追加されるたびにハッシュが自動更新
次のPolyfill.io級インシデントが起きたときにユーザーのデータを守れるかどうかは、今日このたった1行を追加するかどうかにかかっています。
参考資料
- Subresource Integrity(MDN)
- Polyfill.io公式CVE-2024-38526
- SansecのPolyfill.ioインシデント分析
- SRI Hash Generator(srihash.org)
- Fontsource — セルフホスティング用フォントnpmパッケージ
私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!
アプリ広報
Dekuが開発したアプリを使ってみてください。Dekuが開発したアプリはFlutterで開発されています。興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。