Subresource Integrityとリソース改ざん検出
CDN が乗っ取られても改ざんスクリプトを実行させない最後の砦。integrity 属性のハッシュ照合手続き、crossorigin が必須になる理由、複数ハッシュとキャッシュの相互作用まで原理から押さえ、SRI を確実に効かせます。
- 1.SRI は script/link の integrity 属性に base64 のハッシュ(sha256/384/512)を埋め、ブラウザがダウンロードしたバイト列を実行・適用の直前に再ハッシュして照合する。1バイトでも違えば破棄しエラーにする。
- 2.クロスオリジン取得には crossorigin 属性が必須。CORS で明示的に許可された応答だけがハッシュ照合の対象になり、不透明(opaque)応答は中身を読めないため常に拒否される。
- 3.1つの integrity に空白区切りで複数ハッシュを並べると、ブラウザは最も強いアルゴリズムを選んで照合する(OR ではない)。照合はキャッシュ済みリソースにも毎回かかり、合致しない再利用を防ぐ。
Subresource Integrity(SRI)は、外部から読み込む <script> や <link rel="stylesheet"> の中身が「期待したバイト列と一致するか」をブラウザに検証させる仕組みです。CDN やミラーが改ざんされても、配信されたファイルが事前に登録したハッシュと食い違えば実行・適用を拒否します。ここでは照合アルゴリズム、crossorigin が必須になる理由、複数ハッシュとキャッシュの相互作用を原理から見ていきます。
なぜハッシュ照合で改ざんを検出できるか
SRI は対象要素の integrity 属性に、アルゴリズム名-base64ダイジェスト という形式のメタデータを埋め込みます。ブラウザはリソースのバイト列をネットワークから受け取った後、実行・適用の直前に同じアルゴリズムでダイジェストを再計算し、属性値と完全一致するかを比較します。一致すれば通常どおり処理し、不一致ならリソースをネットワークエラーとして破棄します。
<script src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>
成立の根拠は暗号学的ハッシュの衝突困難性です。SHA-256/384/512 は、同じダイジェストを持つ別バイト列を現実的な計算量で見つけられないよう設計されています。したがって攻撃者が CDN 上のファイルを書き換えれば、ダイジェストはほぼ確実に変わり、照合で弾かれます。逆に言えば、SRI が守るのは「配信経路で中身がすり替わっていないこと(完全性)」であって、元のファイル自体が安全かどうか(出自・意図)は保証しません。
ハッシュは HTTP 応答ボディのバイト列そのものに対して計算されます。ただし Content-Encoding(gzip/br など)で圧縮された応答は、ブラウザが解凍した後のバイト列が照合対象です。サーバーが圧縮方式を変えても解凍後が同一なら一致し、改行コードや BOM が1つ変われば不一致になります。圧縮の詳細は コンテンツエンコーディングと転送時圧縮の仕組み を参照してください。
crossorigin が必須になる理由
SRI で最もつまずくのが crossorigin 属性です。クロスオリジンのリソースに SRI をかけるなら crossorigin は必須で、付け忘れると照合できずに読み込みが失敗します。理由はブラウザのオリジン分離にあります。
通常の <script src> はクロスオリジンでも取得できますが、その応答は opaque(不透明)応答 として扱われ、JavaScript からもブラウザ内部の検査からも中身(ボディのバイト列)を読めません。これは 同一オリジンポリシーとサイト分離の信頼境界 が、別オリジンのデータ漏洩を防ぐための原則的な制約です。中身が読めなければハッシュも計算できないため、SRI は成立しません。
そこで crossorigin="anonymous" を付けると、ブラウザはこの取得を CORS(オリジン間リソース共有) モードで行い、サーバーが Access-Control-Allow-Origin を返して明示的に共有を許可した場合にのみ、応答を不透明でなくします。これでブラウザはバイト列を読めるようになり、ハッシュ照合が可能になります。
crossorigin を付けても、配信元 CDN が Access-Control-Allow-Origin を返さなければ応答は不透明のままで、照合できずリソースは読み込まれません。SRI を使う前提として、CDN が CORS を有効にしていることを確認する必要があります。一方、同一オリジンのリソースに SRI をかける場合は中身を読めるため crossorigin は不要です。
crossorigin には anonymous と use-credentials があり、Cookie やクライアント証明書を CORS リクエストに乗せるかどうかが違います。CDN 上の公開ライブラリは通常 anonymous(資格情報を送らない)が適切です。
複数ハッシュとアルゴリズム選択
integrity には空白区切りで複数のダイジェストを並べられます。これは「どれか1つに一致すればよい」という OR ではありません。ブラウザは並んだものの中から最も強いアルゴリズムを選び、その1種類だけで照合します。
<!-- ブラウザは sha384 を選んで照合する。sha256 は無視される -->
<script src="https://cdn.example.com/lib.js"
integrity="sha256-abc... sha384-def..."
crossorigin="anonymous"></script>
強さの優先順位は sha512 > sha384 > sha256 です。同じアルゴリズムのダイジェストを複数並べた場合のみ、そのどれか1つに一致すれば許可されます。これはリソースの差し替え期(旧版と新版の両方が出回る移行期)に、両バージョンのハッシュを並記して切り替えを滑らかにするための仕組みです。
| integrity の記述 | ブラウザの挙動 | 用途 |
|---|---|---|
| sha256-A | sha256 のみで A と照合 | 最小構成。ただし強度は最低 |
| sha256-A sha384-B | sha384 のみで B と照合(sha256 は無視) | 強いアルゴリズムが自動選択される |
| sha384-A sha384-B | sha384 で A か B のどちらかに一致で許可 | 新旧2版の移行期に併記 |
| sha384-A sha512-B | sha512 のみで B と照合 | 最強アルゴリズムが優先される |
試験や面接で問われやすいのが、空白区切りの複数ハッシュの解釈です。異なるアルゴリズムを並べると最も強い種別が選ばれ、弱い種別は完全に無視されます。同一種別を並べたときだけ「いずれかに一致で OK」になります。最低でも sha384 を推奨、というのは sha256 が将来的に弱くなる懸念に備えた指針です。
キャッシュ・プリロードとの相互作用
SRI の照合は毎回のリソース利用時に行われます。ブラウザがメモリキャッシュやディスクキャッシュ、あるいは HTTP キャッシュ からリソースを再利用する場合でも、保持しているバイト列に対して integrity を照合します。したがって、同じ URL でも integrity の値が変われば、キャッシュ済みの古い中身は不一致となり再取得が走ります。これがキャッシュバスティングと同様の「ハッシュ更新=強制再検証」の効果を生みます。
プリロード(<link rel="preload"> や <link rel="modulepreload">)との組み合わせには注意が要ります。プリロードした応答を後続の <script> が再利用するには、両者の取得条件が一致していなければなりません。とくに crossorigin の有無が食い違うと、別々の取得として扱われ二重ダウンロードになります。SRI をかける本番の <script> が crossorigin="anonymous" なら、プリロード側にも同じ crossorigin を付けて取得条件を揃える必要があります。
integrity 照合に失敗したスクリプトはネットワークエラーとして扱われ、onerror が発火します。ブラウザは自動で別ソースを試したりはしません。CDN 障害時の冗長化が必要なら、onerror で自前にローカルコピーへ差し替えるフォールバックを書く設計が定番です。なお照合違反のレポートを集めたい場合は、Integrity-Policy 応答ヘッダと Reporting API を組み合わせると integrity-violation 型のレポートを受け取れます(ただし対応ブラウザは限定的です)。
SRI が守る範囲と守らない範囲
SRI はあくまで「登録済みハッシュと配信バイト列の一致」を検証する仕組みであり、万能ではありません。範囲を正確に押さえることが、過信による事故を防ぎます。
| 脅威 | SRI で防げるか | 理由 |
|---|---|---|
| CDN が改ざんされ別コードを配信 | 防げる | ダイジェストが変わり照合で破棄される |
| 中間者がHTTPS応答を書き換え | 防げる | TLSに加え二重に完全性を担保 |
| 元ライブラリ自体が悪意あるコード | 防げない | 正規ハッシュなら一致してしまう |
| 対象が頻繁に更新される動的JS | 向かない | 更新のたびにハッシュ差し替えが必要 |
| XSSによるインライン注入 | 防げない | SRIはsrc付き外部リソース専用 |
とくに重要なのは、SRI がバージョン固定を前提とする点です。CDN が同じ URL で内容を更新する運用(latest を指す等)では、更新のたびに integrity を書き換えねばならず、書き換え漏れがあれば正規の更新版まで弾かれます。だから SRI はバージョン番号を URL に含めた不変(immutable)なファイルに対してかけるのが原則です。
また SRI は注入されたインラインスクリプトを防げません。これは Content Security Policyの内部動作と回避耐性 の領域です。実務では SRI(外部リソースの完全性)と CSP(実行可能ソースの制限)を組み合わせ、SRI 自体を強制する Integrity-Policy ヘッダや CSP の script-src 厳格化と重ねることで、外部依存と注入の両面を抑えます。
SRI が保証するのは「配信されたものが登録時と同じバイト列であること」だけです。登録した元ファイルが既にバックドア入りなら、ハッシュは正しく一致し、SRI は何も警告しません。サプライチェーン攻撃(依存ライブラリ自体の汚染)への対策にはならない点を、設計時に明確に区別してください。SRI は配信経路の改ざん検出に特化した一枚であり、依存監査・署名検証・CSP と多層で組み合わせて初めて意味を持ちます。
まとめ
SRI は integrity 属性のハッシュ(sha256/384/512、base64)を、ブラウザが受信バイト列の再計算ダイジェストと照合し、1バイトでも違えば実行・適用を拒否する完全性検証です。クロスオリジンでは中身を読むために crossorigin と CDN 側の CORS 許可が必須で、不透明応答は常に拒否されます。複数ハッシュは最も強い種別が自動選択され(同種併記のときだけ OR)、照合はキャッシュ再利用時にも毎回かかります。守れるのは配信経路の改ざんで、元ファイルの汚染やインライン注入は守れないため、不変ファイルへの適用と CSP・依存監査との多層化が前提になります。
Web/フロントエンド Article
Subresource Integrityとリソース改ざん検出を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
セキュリティ
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 6
導入後に効く点
クロスオリジン取得には crossorigin 属性が必須。CORS で明示的に許可された応答だけがハッシュ照合の対象になり、不透明(opaque)応答は中身を読めないため常に拒否される。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 6
判断チェックリスト
- 自社の用途が「セキュリティ / Web」に近いか確認する。
- 強みである「SRI は script/link の integrity 属性に base64 のハッシュ(sha256/384/512)を埋め、ブラウザがダウンロードしたバイト列を実行・適用の直前に再ハッシュして照合する。1バイトでも違えば破棄しエラーにする。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。