SRI(Subresource Integrity)とは — CDN 経由の改ざんを防ぐ
SRI(Subresource Integrity)は、CDN など外部から読み込むスクリプトやスタイルシートの中身が改ざんされていないかを、ハッシュ値で検証する仕組みです。ブラウザはダウンロードしたファイルのハッシュを計算し、integrity 属性に書かれた値と一致しなければ、そのコードの実行や適用を拒否します。CDN が乗っ取られて配信ファイルをすり替えられても、被害を防げます。
SRI(Subresource Integrity)とは何ですか?
SRI は、CDN など外部から読み込むスクリプトやスタイルシートの中身が改ざんされていないかを、ハッシュ値で検証する仕組みです。ブラウザはダウンロードしたファイルのハッシュを計算し、integrity 属性に書かれた値と一致しなければ、そのコードの実行や適用を拒否します。CDN が侵害されてファイルをすり替えられても、改ざんされたコードが動くのを防げます。
いまどきのサイトは、jQuery や解析タグ、Web フォントなどを自前サーバーではなく外部の CDN から読み込むことが珍しくありません。 便利で速い一方で、その CDN が配っているファイルの中身を、こちらは制御できないという弱点があります。 もし CDN 側が侵害され、配信ファイルに悪意あるコードが混ぜ込まれたら、それをそのまま読み込むあなたのサイト上で不正なコードが実行されてしまいます。
SRI は、この「読み込んだファイルが本当に意図したものか」をブラウザに検証させます。仕組みは 3 段階です。
- ハッシュを埋め込む — タグの
integrity属性に、正しいファイルのハッシュ値をあらかじめ書いておく - ブラウザが再計算 — 実際にダウンロードした中身のハッシュを、ブラウザがその場で計算する
- 照合する — 2 つが一致すれば実行・適用し、1 バイトでも違えば拒否する
ハッシュは中身が少しでも変われば全く別の値になるため、改ざんは確実に検知できます。自前サーバーで配るファイルには通常 SRI は不要で、意味を持つのは外部から取り込むリソースです。
SRI は何を防ぎますか?
外部の CDN から配信されるファイルがすり替えられる、サプライチェーン攻撃を防ぎます。人気ライブラリを配る CDN が侵害されると、それを読み込む何千ものサイトへ一斉に悪意あるコードが流れ込みます。SRI があれば、配信内容がハッシュと一致しない時点でブラウザが読み込みを止めるため、改ざんされたコードは実行されません。
守る相手はサプライチェーン攻撃、つまり「自分のサーバーではなく、自分が依存している外部の供給元が乗っ取られる」タイプの攻撃です。 広く使われる CDN やライブラリは、1 か所を汚染するだけで無数のサイトへ波及するため、攻撃者にとって効率の良い標的になります。 2024 年の polyfill.io の一件では、あるドメインが第三者に渡り、それを読み込んでいた多数のサイトへ不正なコードが配信される事態になりました。読み込む側が integrity を付けていれば、すり替わったファイルはハッシュ不一致でブロックされ、実行されません。
| 状況 | integrity あり | integrity なし |
|---|---|---|
| CDN が正常 | ハッシュ一致 → 実行 | そのまま実行 |
| ファイルがすり替えられた | ハッシュ不一致 → ブロック(安全) | 改ざんコードを実行(危険) |
| 正規のバージョン更新 | ハッシュ不一致 → 読み込み失敗 | 新しい版を実行 |
最終行が示すとおり、SRI は「良い変更」と「悪い変更」を区別しません。あらかじめ知らせた中身と 1 バイトでも違えば止めるだけです。 この性質が、後述するバージョン固定との併用が必須になる理由につながります。
integrity 属性はどう書きますか?
script や link タグに integrity 属性を足し、アルゴリズム名とハッシュ値をハイフンでつないで書きます。値は sha256・sha384・sha512 のいずれかで生成した Base64 文字列で、sha384-… の形になります。外部リソースには合わせて crossorigin="anonymous" を付ける必要があります。複数のハッシュを半角スペース区切りで併記することもできます。
<!-- 外部 CDN: integrity でハッシュ検証、crossorigin で CORS 取得 -->
<script
src="https://cdn.example.com/lib@1.4.2/lib.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6R9GqQ8Kuxy9rx7HNQlGYl1kPzQho1wx4JwY8wCx"
crossorigin="anonymous"></script>
<!-- スタイルシートも同じ要領で検証できる -->
<link
rel="stylesheet"
href="https://cdn.example.com/lib@1.4.2/lib.min.css"
integrity="sha384-9ndCyUa8m9uQp8vQ1z5oQ6f6f1cPnT8Qp3n2s0kY0wq1Hm9Xy0vN3cB2aL4dR6tP"
crossorigin="anonymous">
<!-- NG: integrity なし。CDN がすり替えられても検知できない -->
<script src="https://cdn.example.com/lib/lib.min.js"></script>integrity の値は 「アルゴリズム名」+「ハイフン」+「Base64 のハッシュ」という形です。 アルゴリズムは sha256・sha384・sha512 から選べ、現在は sha384 が広く使われます。 より強いものを使いたければ sha512 でも構いません。半角スペースで区切って複数のハッシュを併記でき、その場合はブラウザが最も強いアルゴリズムを優先し、いずれか 1 つに一致すれば合格とみなします。移行期に新旧のハッシュを同時に許可したいときに使えます。
なぜ crossorigin 属性が必要なのですか?
SRI は CORS で取得されたリソースにしか働かないため、crossorigin 属性が欠かせません。crossorigin="anonymous" を付けると、ブラウザは Cookie を送らない匿名の CORS リクエストで取得し、中身をハッシュ検証できる状態にします。これが無いと、たとえ integrity を書いてもブラウザは中身を検証できず、そのリソースの読み込み自体を拒否します。
ここが SRI で最もつまずきやすい点です。SRI は CORS で取得されたリソースにしか働きません。 別オリジンのファイルを検証するには、その中身をスクリプトから読める形で取得する必要があり、そのために crossorigin 属性で CORS リクエストであることを明示します。通常は Cookie などの資格情報を送らない crossorigin="anonymous" を使います。
属性を付けても、配信元の CDN が Access-Control-Allow-Origin レスポンスヘッダを返していなければ CORS は成立しません。 幸い主要な CDN はこのヘッダを付けて配信しているので実務ではほとんど問題になりませんが、自前や小規模なストレージから配る場合は、 そのサーバー側で CORS を許可する設定が別途必要になります。integrity を書いたのに読み込めないときは、まずここを疑ってください。 関連する仕組みは CORS の解説も参照してください。
バージョン固定 URL と併用すべきなのはなぜですか?
integrity は特定の中身に対するハッシュなので、内容が変わらない固定バージョンの URL と併用します。@latest のような可変 URL は、配信側がファイルを更新した瞬間にハッシュが合わなくなり、正規の更新でも読み込みに失敗します。バージョンを固定した URL を指定し、更新するときは URL とハッシュを両方まとめて差し替えるのが正しい運用です。
integrity は「今この瞬間の、この中身」に対するハッシュです。したがって、指す先の中身が将来変わる URL を指定すると、 正規の更新であってもハッシュが合わなくなり、ある日突然そのファイルが読み込めなくなります。lib@latest/lib.js のような可変 URL や「常に最新を返す」パスは、SRI とは相性が悪いということです。
正しくは、lib@1.4.2/lib.min.js のようにバージョンを固定した URLを指定し、そのバージョンの中身に対応したハッシュを書きます。 ライブラリを新しい版に上げるときは、URL のバージョンと integrity のハッシュを、必ずセットで差し替えるのが運用の基本です。 どちらか片方だけ変えると、ハッシュ不一致で読み込みが止まります。
integrity に書くハッシュはどう生成しますか?
配信されるファイルそのものに対して sha384 などでハッシュを計算し、Base64 で表した文字列を使います。openssl コマンドや SRI Hash Generator のような Web ツールで生成できます。多くの CDN やライブラリの配布ページは、コピーするだけで使える integrity 付きのタグを提供しています。自前でビルドする資産なら、ビルド時に自動生成する方法もあります。
# 手元のファイルの sha384 ハッシュを Base64 で得る
cat lib.min.js | openssl dgst -sha384 -binary | openssl base64 -A
# 出力された文字列を "sha384-" の後ろに貼る
# 例) oqVuAfXRKap7fdgcCY5uykM6R9GqQ8Kuxy9rx7HNQlGYl1kPzQho1wx4JwY8wCx
# 配信 URL から直接生成する場合
curl -s https://cdn.example.com/lib@1.4.2/lib.min.js \
| openssl dgst -sha384 -binary | openssl base64 -Aコマンドが手元に無くても、多くの CDN やライブラリの配布ページはコピーするだけで使える integrity 付きのタグを用意しています。 その場合は生成の手間すら要りません。srihash.org のような Web ツールに URL を入れて生成することもできます。 自前でビルドする JavaScript や CSS を SRI 対応させたい場合は、ビルドツールのプラグインで出力時にハッシュを自動計算して埋め込むのが現実的です。
SRI が効いているかどう確認しますか?
ブラウザの開発者ツールで、integrity 属性の有無と、検証失敗時のエラーを確認します。わざとハッシュを 1 文字書き換えて再読み込みすると、コンソールに整合性エラーが出てリソースがブロックされれば、検証が働いている証拠です。本ツールの診断でも、外部スクリプトに integrity が付いているかを検出します。
動作確認は簡単です。開発者ツールを開いた状態で、対象タグの integrity の値をわざと 1 文字書き換えて再読み込みしてみてください。 検証が働いていれば、コンソールに整合性エラーが表示され、そのリソースはブロックされます。逆に何も起きなければ、 そもそも crossorigin が付いていないなどの理由で検証が働いていない可能性があります。
SRI は「付けられるところに付ける」だけで、外部リソース経由の改ざんという厄介な経路を一つ塞げる、費用対効果の高い対策です。 同じ外部リソースの制御という文脈では、CSP やセキュリティヘッダと組み合わせると、守りの層がさらに厚くなります。