外部のCDNから読み込んでいるJavaScriptやCSSは、自分のサーバーには一切手を加えなくても、配信元が書き換わればそのまま実行されてしまいます。2024年のpolyfill.io事件では、CDN(コンテンツ配信ネットワーク=世界中のサーバーからファイルを配る仕組み)経由で配られるスクリプトに悪意あるコードが混入し、10万を超えるサイトが巻き込まれました。
この記事は、外部CDNのJS/CSSを使っているWeb担当者・制作会社向けに、読み込んだファイルが改ざんされていないかをブラウザ自身に検証させる仕組み SRI(Subresource Integrity) の設定と運用をまとめたものです。integrity 属性を1行足すだけで、ハッシュが一致しないファイルは読み込みが拒否されます。HTMLの直書きとWordPressの両方のコピペ設定を載せます。
SRI(Subresource Integrity)とは
SRIは、<script> や <link> に そのファイルの正しいハッシュ値(指紋) を書いておく仕組みです。ブラウザはファイルを取得したあと、実行・適用する前に中身のハッシュを計算し、integrity 属性に書かれた値と突き合わせます。
- 一致すれば、いつもどおり実行/適用する
- 一致しなければ、読み込みを拒否してエラーにする(改ざん・差し替えを検知)
つまり「配信元が信頼できるか」ではなく「受け取ったファイルが、自分が想定したそのファイルか」を毎回チェックします。CDNが乗っ取られてファイルが差し替えられても、ハッシュが変わるためブラウザ側で止まります。W3Cの標準仕様で、主要ブラウザが対応しています(参考: MDN — Subresource Integrity)。
書き方:integrity と crossorigin をセットで付ける
integrity 属性の値は <ハッシュ関数>-<Base64ハッシュ> の形式です。ハッシュ関数は sha256 / sha384 / sha512 が使え、sha384 が推奨です(短すぎず、広く実装されているため)。
外部(別オリジン)から読む場合は、crossorigin="anonymous" が必須です。これが無いとブラウザは中身の検証ができず、そのままではリソースがブロックされます。anonymous はCookieを送らずに取得する指定で、公開CDNのライブラリにはこれを使います。
<!-- JavaScript を CDN から読む場合 -->
<script src="https://cdn.example.com/lib/1.2.3/lib.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>
<!-- CSS を CDN から読む場合 -->
<link rel="stylesheet"
href="https://cdn.example.com/lib/1.2.3/lib.min.css"
integrity="sha384-9ndCyUa8mVdBH8VV3eZ1RygMOtwaU5uyMfQqQ0l1c8YpQ0i8bJf1kPzQho1wx4JwY8w"
crossorigin="anonymous">
integrity の値はファイルごとに異なる指紋なので、上の例のハッシュはあくまでダミーです。実際に使うファイルから、次の手順で生成してください。
- 前提として、リソースは必ずHTTPSで読み込むこと。HTTP混在があるとそもそも改ざんの余地が残ります(混在コンテンツの解消は後述の関連記事を参照)。
ハッシュの生成方法(openssl / Webツール)
対象ファイルを取得して sha384 を計算し、Base64にします。openssl があればコマンド1行です。
# 対象ファイルをそのまま取得して sha384 → Base64
curl -s https://cdn.example.com/lib/1.2.3/lib.min.js \
| openssl dgst -sha384 -binary \
| openssl base64 -A
# 出力例: oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC
出力された文字列の先頭に sha384- を付けたものが integrity の値になります。
ローカルにファイルがある場合は、curl の代わりにファイルを直接渡します。
openssl dgst -sha384 -binary lib.min.js | openssl base64 -A
コマンドを使わずに生成したい場合は、公式のWebツール SRI Hash Generator(srihash.org) にファイルのURLを入れると、integrity と crossorigin が付いた <script> タグをそのまま出力してくれます。
注意: ハッシュは必ず自分で計算した値を使い、配信元が「これを貼ってください」と提示してきた値を無検証でコピーしないこと。ファイルとハッシュの両方を攻撃者が用意できてしまうと、検証の意味がなくなります。信頼できる時点のファイルから、自分の手元で生成するのが原則です。
WordPress で SRI を付ける(functions.php)
WordPressでCDNのライブラリを読み込む場合、テーマやプラグインが wp_enqueue_script() / wp_enqueue_style() で出力する <script> <link> タグに、フィルタで integrity と crossorigin を差し込みます。ハンドル名(登録名)とハッシュの対応表を持たせるのがコピペしやすい形です。
<?php
// functions.php — CDN で読み込むスクリプト/スタイルに SRI を付与する
// ハンドル名 => sha384 ハッシュ の対応表(自分で生成した値に置き換える)
function my_sri_map() {
return array(
// 'ハンドル名' => 'sha384-...'
'my-cdn-lib' => 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC',
'my-cdn-css' => 'sha384-9ndCyUa8mVdBH8VV3eZ1RygMOtwaU5uyMfQqQ0l1c8YpQ0i8bJf1kPzQho1wx4JwY8w',
);
}
// <script> に integrity / crossorigin を差し込む
add_filter('script_loader_tag', function ($tag, $handle, $src) {
$map = my_sri_map();
if (isset($map[$handle])) {
$attr = ' integrity="' . esc_attr($map[$handle]) . '" crossorigin="anonymous"';
$tag = str_replace(' src=', $attr . ' src=', $tag);
}
return $tag;
}, 10, 3);
// <link>(CSS) に integrity / crossorigin を差し込む
add_filter('style_loader_tag', function ($tag, $handle, $href, $media) {
$map = my_sri_map();
if (isset($map[$handle])) {
$attr = ' integrity="' . esc_attr($map[$handle]) . '" crossorigin="anonymous"';
$tag = str_replace(' href=', $attr . ' href=', $tag);
}
return $tag;
}, 10, 4);
対応表のハンドル名は、wp_enqueue_script('my-cdn-lib', 'https://cdn.example.com/...') のように登録したときの第1引数です。自ホストしている自作JS/CSSにはSRIは不要(自分のサーバーが改ざんされたらSRIの前に別の問題です)。SRIは外部から読み込むファイルに絞って付けます。
運用の落とし穴:壊れる付け方をしない
SRIは強力ですが、「バージョンが固定された、中身が変わらないURL」にしか付けられません。ここを外すと正常なサイトが真っ白になります。
-
自動更新エンドポイントには付けない。
@latestのように常に最新を返すURLや、リクエストに応じて中身を生成するサービス(polyfill系など)は、中身が変わるたびにハッシュが合わずブロックされます。これらはバージョンを固定したURLに変えるか、ファイルを自ホストするのが安全です。実は polyfill.io 事件で被害が広がった一因も「中身が変わりうるものを外部にそのまま任せていた」点にあります。 -
ライブラリを更新したらハッシュも再生成する。 バージョンを上げるとファイルの中身が変わるので、
integrityの値も必ず作り直します。更新手順に「ハッシュ再生成」を組み込んでおくと事故りません。 -
ローテーション時は複数ハッシュを併記できる。 新旧2つのファイルが混在しうる移行期間は、
integrity="sha384-旧 sha384-新"のようにスペース区切りで複数指定でき、どちらか一致すれば通ります。 -
反映後は必ず実機で表示確認する。 付け間違い(ハッシュのコピペミス、
crossoriginの付け忘れ)があると、そのリソースだけ読み込まれず機能が欠けます。公開前にブラウザで確認します。
CSPとの二段構え
SRIと CSP(Content Security Policy) は役割が違い、併用すると守りが厚くなります。
- CSP … スクリプトやスタイルを「どこから読み込んでよいか」を制限する(読み込み元の許可リスト)
- SRI … 読み込んだファイルの「中身が本物か」を検証する(改ざん検知)
CSPで信頼できる配信元だけに絞り、そのうえでSRIで受け取ったファイルの指紋を確認する、という二段構えにすると、「許可した配信元が乗っ取られて中身をすり替えられた」ケースまでカバーできます。CSPの段階導入は後述の関連記事にまとめています。
確認方法
ハッシュ不一致を意図的に起こして、ブロックされることを確認しておくと安心です。integrity の値を1文字だけ変えて表示すると、ブラウザの開発者ツール(F12)のConsoleに次のような警告が出て、そのリソースが読み込まれません。
Failed to find a valid digest in the 'integrity' attribute for resource
'https://cdn.example.com/lib/1.2.3/lib.min.js' with computed SHA-384 integrity '...'.
The resource has been blocked.
このメッセージが出れば、SRIが正しく効いています。確認後は integrity の値を正しいものに戻してください。正しく設定できていれば、通常時は何のエラーも出ずに今までどおり動きます。
まとめ
- 外部CDNのJS/CSSは、配信元が書き換われば自サイトの改修なしに悪意あるコードが動く。SRIはその中身の改ざんをブラウザに検知させる仕組み。
-
<script><link>にintegrity="sha384-..."とcrossorigin="anonymous"をセットで付ける。ハッシュはopensslかsrihash.orgで自分で生成する。 - WordPressは
script_loader_tag/style_loader_tagフィルタで対応表を使って付与できる。 - バージョン固定URLにだけ付ける(自動更新エンドポイントは壊れる→自ホスト推奨)。更新時はハッシュを再生成。移行期は複数ハッシュ併記。
- CSP(読み込み元を絞る)と組み合わせると、配信元乗っ取り+中身すり替えまで守れる。
関連記事
本記事のような設定の抜けは、Webサイトを9つの守りでまるごと守るサイトドックでまとめて対策できます → https://sitedock.jp