Subresource Integrity (SRI) 完全ガイド|CDN スクリプトの integrity・crossorigin・フォールバック
Subresource Integrity(SRI)とは
Subresource Integrity(SRI)は、<script> や <link rel="stylesheet"> など外部オリジンから取得するサブリソースについて、ダウンロードした内容が開発者が期待したバイト列と一致するかをブラウザが検証する仕組みだ。
HTML では integrity 属性に 暗号学的ハッシュ(通常 sha256 / sha384 / sha512) をベース64 エンコードした値を書く。一致すれば通常どおり実行・適用され、不一致ならブラウザはブロックし Console にエラーを出す。
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js"
integrity="sha384-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
crossorigin="anonymous"
></script>
SRI は W3C Subresource Integrity として標準化され、Chrome・Firefox・Safari・Edge すべてでサポートされている。2026 年の新規 Web アプリでは、公開 CDN から script を直読みする箇所に SRI が無い状態は、セキュリティレビューで指摘されることが多い。
SRI が防ぐ脅威
SRI 単体では XSS 注入そのものは防げない。しかし次のサプライチェーン改ざんシナリオでは決定的に効く。
| 脅威 | 説明 | SRI の効果 |
|---|---|---|
| CDN アカウント侵害 | 攻撃者が CDN 上のファイルを差し替え | ハッシュ不一致で実行拒否 |
| 中間者攻撃(MITM) | TLS 突破または混在コンテンツ経由の改ざん | 内容検証で検出 |
| npm / パッケージレジストリ事故 | 悪意あるバージョンが CDN ミラーに反映 | 固定バージョン + 固定ハッシュで遮断 |
| キャッシュ汚染 | 共有 CDN キャッシュへの不正レスポンス混入 | オリジン取得後のハッシュ検証で防御 |
OWASP Top 10 A08:2021 – Software and Data Integrity Failures は、まさにこの「信頼していた外部コードが想定と異なる」問題を扱う。XSS としての被害像(Cookie 窃取・DOM 操作)は XSS 攻撃と防御の完全ガイド と同じだが、入口が自サイトの入力ではなく第三者 CDN である点が異なる。
入力のエスケープ・CSP nonce・HttpOnly Cookie と並べ、SRI は読み込むファイルの中身を固定する層だ。どれか一つだけでは不十分で、多層防御として組み合わせる。
integrity 属性の書式
integrity 値は次の形式になる。
アルゴリズム-ベース64ハッシュ
複数のハッシュをスペース区切りで並べると、ブラウザはいずれか一つが一致すれば受理する。アルゴリズム移行期に旧 hash と新 hash を併記する用途がある。
<link
rel="stylesheet"
href="https://cdn.example.com/app.v2.css"
integrity="sha384-OLD_HASH_BASE64 sha384-NEW_HASH_BASE64"
crossorigin="anonymous"
/>
推奨アルゴリズム(2026)
| アルゴリズム | 推奨度 | 備考 |
|---|---|---|
| sha384 | 最推奨 | W3C 仕様のデフォルト推奨。性能と強度のバランスが良い |
| sha512 | 可 | やや長いが許容 |
| sha256 | 可 | レガシー互換・短い hash |
| md5 / sha1 | 非推奨 | 衝突耐性が不十分。新規では使わない |
<script type="module"> や importmap 経由のモジュールにも integrity は指定できるが、動的 import() の URL 文字列には HTML 属性として付けられない。バンドラ(Vite・webpack)が生成する <script type="module"> にビルド時 integrity を埋め込む方式が現実的だ。
crossorigin 属性が必須な理由
クロスオリジンリソースに integrity だけ付けても、検証は行われないことがある。ブラウザは CORS チェックとセットでサブリソースの生バイト列を integrity 計算に使うため、crossorigin が必要になる。
| crossorigin の値 | 挙動 |
|---|---|
| anonymous(または空文字) | Cookie を送らない CORS リクエスト。公開 CDN の script / CSS で最も一般的。Access-Control-Allow-Origin: * と相性が良い |
| use-credentials | Cookie 付きで取得。CDN が Access-Control-Allow-Credentials: true と具体オリジンを返す必要がある。認証付き private CDN 向け |
| 属性なし | no-cors モードになり、クロスオリジンでは integrity 検証がスキップされる——SRI 無効 |
CDN 側では次のレスポンスヘッダーが必要になることが多い。
Access-Control-Allow-Origin: *
Timing-Allow-Origin: *
jsDelivr・unpkg・cdnjs など主要 npm CDN は Access-Control-Allow-Origin: * を返すため、crossorigin="anonymous" と組み合わせやすい。自社 CDN を立てる場合は、SRI を使うオリジンすべてで CORS ヘッダーを忘れないこと。
DevTools 上は script が 200 で読み込まれているのに、integrity が効いていない——という状態になり得る。セキュリティ監査用チェックリストに「外部 script に integrity と crossorigin の両方があるか」を入れる。
integrity ハッシュの生成方法
ハッシュは実際に配信されるバイト列に対して計算する。gzip / Brotli 圧縮は転送時のみで、ブラウザが integrity 計算に使うのは展開後の本文だ。CDN から取得したファイルをそのまま hash すればよい。
openssl(Linux / macOS / Git Bash)
curl -fsSL "https://cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js" \
-o /tmp/react.production.min.js
openssl dgst -sha384 -binary /tmp/react.production.min.js | openssl base64 -A
出力例(実際の値は上記コマンドで取得する):
integrity="sha384-(ここにベース64文字列)"
Windows PowerShell
$url = "https://cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js"
$bytes = (Invoke-WebRequest -Uri $url -UseBasicParsing).Content
$hash = [System.Security.Cryptography.SHA384]::Create().ComputeHash($bytes)
$base64 = [Convert]::ToBase64String($hash)
Write-Output "sha384-$base64"
Node.js ワンライナー
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
const file = readFileSync('/tmp/react.production.min.js');
const base64 = createHash('sha384').update(file).digest('base64');
console.log(`sha384-${base64}`);
shasum(macOS 付属)
shasum -b -a 384 /tmp/react.production.min.js | awk '{print $1}' | xxd -r -p | base64
macOS の base64 は改行を付ける場合がある。HTML 属性用は -A(一行)相当の出力に揃える。
オンライン CDN が integrity を提供する例
jsDelivr はファイルページから SRI 用の integrity 文字列をコピーできる。手動計算と突合して CI に取り込むとミスが減る。
<!-- jsDelivr の例: バージョンとファイルパスを固定 -->
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"
integrity="sha384-(CDN ページまたは openssl で取得)"
crossorigin="anonymous"
></script>
アプリ自身の app.[hash].js を CDN に載せる場合も、デプロイパイプラインでアップロード直後のファイルから hash を計算し、HTML テンプレートまたは manifest に書き戻す。Cloudflare Pages / Vercel などはビルド時にファイル名へ content hash を付けるが、それとは別に Subresource Integrity 用の属性が HTML に必要かどうかはテンプレート設計次第だ。
バージョン固定と CDN URL 設計
SRI と相性が悪い URL パターンがある。
| URL パターン | SRI との相性 | 理由 |
|---|---|---|
/npm/[email protected]/umd/react.production.min.js | 良 | パスに semver 固定 |
/npm/react@18/umd/react.production.min.js | 中 | メジャー固定はマイナー更新で中身が変わり得る |
/npm/react/latest/... | 悪 | デプロイ後いつでも hash が無効化 |
/npm/react@^18.3.1/...(semver range) | 悪 | 解決結果が変動 |
原則: SRI を付ける URL は immutable として扱う。 npm なら exact version、自社 CDN なら /assets/app-20260622-a1b2c3.js のようにファイル名にビルド ID を含める。
package.json の dependencies を更新したタイミングで、HTML に埋めた integrity も同じ PR で更新する。Dependabot が package だけ上げて integrity が古いまま、という PR は CI で落とす。
HTML への適用例
複数 CDN スクリプト
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>SRI サンプル</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/modern-normalize.min.css"
integrity="sha384-(modern-normalize の hash)"
crossorigin="anonymous"
/>
</head>
<body>
<div id="root"></div>
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js"
integrity="sha384-(react の hash)"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.production.min.js"
integrity="sha384-(react-dom の hash)"
crossorigin="anonymous"
></script>
<script src="/app.js" defer></script>
</body>
</html>
同一オリジン /app.js に integrity を付けることもできる。自前ホストなら改ざん検知に有効だが、デプロイのたびに hash 更新が必要になる。ビルドツールが自動注入するか、ファイル名に content hash を付けて src 自体を変える方が運用しやすい。
ES modules(importmap + SRI)
<script type="importmap">
{
"imports": {
"vue": "https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.esm-browser.prod.js"
}
}
</script>
<script type="module" crossorigin="anonymous">
import { createApp } from 'vue';
createApp({ template: '<p>Hello</p>' }).mount('#app');
</script>
importmap 内 URL への integrity は仕様拡張中であり、2026 年時点では importmap の各 URL に integrity を直接書けないケースがある。モジュール CDN を使う SPA は、Vite / webpack でバンドルし <script type="module" integrity="..."> をビルド出力に含める方が確実だ。
検証失敗時のフォールバック戦略
integrity 不一致時、ブラウザは script を実行しない。ユーザーから見ると真っ白な画面や機能停止になる。可用性とセキュリティのバランスを取るフォールバックパターンを整理する。
| 戦略 | メリット・注意点 |
|---|---|
| 自前ホスト ミラー | CDN 障害・改ざん時に同一バージョンを /static/vendor/ から配信。事前に integrity 検証済みファイルをデプロイに含める |
| onerror 動的読み込み | <script onerror="loadFallback()"> で第二候補 URL へ。第二候補にも integrity を付けるか、'self' のみ CSP で限定 |
| Service Worker キャッシュ | 過去に検証済みのレスポンスを返す。初回訪問者には効かない。更新戦略が複雑 |
| エラー UI 表示 | フォールバックせず「メンテナンス中」を出す。金融系など改ざんより停止を選ぶ場合 |
onerror フォールバックの実装例
<script
id="vendor-react"
src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js"
integrity="sha384-PRIMARY_HASH"
crossorigin="anonymous"
onerror="window.__loadVendorFallback('react', '18.3.1')"
></script>
<script>
window.__loadVendorFallback = function (pkg, version) {
var s = document.createElement('script');
s.src = '/vendor/' + pkg + '@' + version + '.min.js';
s.crossOrigin = 'anonymous';
s.integrity = 'sha384-FALLBACK_HASH_FOR_SELF_HOSTED_COPY';
document.head.appendChild(s);
};
</script>
フォールバック script を無 integrity で読み込むのは、第二 CDN も侵害されている場合に同じリスクを抱える。自前ホストのファイルはデプロイ時に hash を固定し、PRIMARY と FALLBACK で同じバイト列を配るのが理想だ。
監視とアラート
本番で SRI 失敗が起きると、ブラウザ Console に次のようなメッセージが出る。
Failed to find a valid digest in the 'integrity' attribute for resource
'https://cdn.example.com/lib.js' with computed SHA-384 digest '...'.
Reporting API や Content-Security-Policy-Report-Only とは別経路のため、Synthetic monitoring で主要ページの script 読み込み成功率を計測するか、RUM で error イベントを集約する。CDN メンテナンスで一斉に hash がずれる前に、ステージングでバージョンアップ PR を検証する。
動的に追加する script の URL は CSP の script-src に含める必要がある。'strict-dynamic' と nonce ベースのポリシーでは、onerror で挿入した script がブロックされる——設計段階で nonce 付与か hash 許可を決める。
CSP(Content Security Policy)との統合
SRI と CSP は補完関係にある。
| レイヤー | 役割 |
|---|---|
| CSP script-src | どのオリジンから script を読み込んでよいか |
| SRI integrity | 許可された URL から取得した中身が期待どおりか |
CSP だけでは https://cdn.jsdelivr.net を許可すると、その CDN 上の任意ファイル(攻撃者が差し替えたファイル含む)も実行され得る。SRI を付ければ、特定バージョンの特定ファイルに実行権を限定できる。
推奨 CSP 例(CDN + 自前バンドル)
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net 'nonce-RANDOM' 'strict-dynamic';
style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline';
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'self';
script-srcに CDN ホストを列挙- アプリ本体は
'self'+ nonce - 各
<script src="https://cdn...">に integrity を付けて中身を固定
CSP の require-sri-for ディレクティブ(require-sri-for script style)は、対応ブラウザで SRI 未設定の外部 script / style をブロックする。ただし 2026 年時点ではブラウザサポートが限定的であり、本番で単独依存するより CI で integrity 必須 lint を回す方が確実だ。
CSP hash ソースとの違い
CSP の 'sha256-...' ソース式はインライン script の内容を許可するためのもの。SRI の integrity は外部 URL 取得結果に対する検証。名前は似ているが用途が異なる。
| 機能 | 対象 | 指定場所 |
|---|---|---|
| CSP hash ソース | インライン <script> 本文 | CSP ヘッダー |
| SRI integrity | 外部 script / link のレスポンス body | HTML 属性 |
両方とも sha256 系だが、混同しないこと。インライン設定用 JSON などは CSP hash、CDN の React UMD などは SRI integrity。
Report-Only との併用
CSP を Report-Only で段階導入 している間も SRI は即座に有効になる。SRI 失敗は CSP レポートには載らないため、別途 E2E テストで確認する。
フレームワーク・ビルドパイプラインでの自動化
手動 integrity 管理はヒューマンエラーの温床だ。2026 年の定石は CI / ビルドで生成する。
package-lock.json または pnpm-lock で CDN URL とバージョンを確定する
postinstall または deploy 前ジョブで CDN からファイルを取得し sha384 hash を計算する
HTML テンプレート・Astro レイアウト・Next.js Document に integrity 属性を注入する
pull request で hash 変更と lockfile 変更がセットかをレビューする
本番反映後、Synthetic check で script 200 + 実行確認を行う
Astro レイアウト例
---
// src/layouts/Base.astro
const reactVersion = '18.3.1';
const reactIntegrity = import.meta.env.PUBLIC_REACT_SRI; // CI で注入
---
<script
src={`https://cdn.jsdelivr.net/npm/react@${reactVersion}/umd/react.production.min.js`}
integrity={reactIntegrity}
crossorigin="anonymous"
></script>
環境変数 PUBLIC_REACT_SRI はデプロイパイプラインで openssl 出力を設定する。
webpack subresource-integrity プラグイン
// webpack.config.js
import SriPlugin from 'webpack-subresource-integrity';
export default {
output: { crossOriginLoading: 'anonymous' },
plugins: [
new SriPlugin({
hashFuncNames: ['sha384'],
enabled: process.env.NODE_ENV === 'production',
}),
],
};
crossOriginLoading: 'anonymous' は output script に crossorigin を付与するために必要。HtmlWebpackPlugin と組み合わせると、バンドルされた <script> に自動で integrity が付く。
CI lint(integrity 欠落検出)
# .github/workflows/sri-check.yml
name: sri-check
on: [pull_request]
jobs:
lint-html:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fail if external script lacks integrity
run: |
! grep -RIn '<script[^>]*src="https://' public/ src/ \
| grep -v 'integrity='
grep ベースは粗いが、PR で「CDN script 追加したのに integrity 無し」を防げる。本格運用では html-validate やカスタム ESLint ルールへ移行する。
2026 年のベストプラクティス checklist
実務でそのまま使えるチェックリスト。
- 公開 CDN script にはすべて integrity + crossorigin=“anonymous”
- URL は semver または content hash で固定 —
latest禁止 - CSP script-src で CDN ホストを最小限に列挙(設定ガイド参照)
- 依存更新 PR では lockfile・integrity・E2E を同一 PR で更新
- 自前ミラー を用意し、onerror または CDN 障害時の Runbook を文書化
- サプライチェーン事故 を XSS 多層防御 の観点で tabletop 訓練に含める
- md5 / sha1 integrity は新規禁止
- importmap 直 CDN は SRI 制約を理解した上で、可能ならバンドルに寄せる
よくある失敗パターン
integrity だけ付けて crossorigin が無い
症状: 本番で「SRI 設定済み」と思い込んでいるが、検証がスキップされている。
対策: HTML linter とコードレビューで crossorigin を必須化。
CDN が Content-Encoding だけ変えた
症状: 同一ファイルのはずが Brotli 配信切替で body が変わり hash 不一致。
対策: hash 計算元はブラウザが解釈する解压後 body。CDN 設定変更後に hash を再計算。
開発環境だけ integrity を外している
症状: staging と production で HTML が diverge し、本番だけ動かない。
対策: 開発でも integrity を付け、Vite の server.proxy でローカルミラーを使う。どうしても外すならビルドフラグを明示し、本番 artifact では必ず有効化。
CSP strict-dynamic と onerror フォールバックの競合
症状: フォールバック script が CSP でブロックされ、復旧不能。
対策: フォールバック URL を script-src に事前登録するか、フォールバック用 script に nonce を付与する SSR 設計にする。
ブラウザ DevTools での確認手順
- Network タブで対象 script を選択
- Headers で
Origin付きリクエストになっているか(crossorigin 効果) - Console に integrity エラーが無いか
- Sources で script が実行されているか
- わざと integrity の末尾 1 文字を改ざんし、ブロックされることを確認
改ざんテストはステージングのみで行う。本番 HTML を直接いじるとキャッシュ伝播でユーザー影響が出る。
SRI と関連ヘッダーの関係
| ヘッダー / 属性 | SRI との関係 |
|---|---|
| Content-Security-Policy | オリジン許可。SRI は中身固定 |
| Cross-Origin-Resource-Policy | CORP が block だと CDN 取得自体が失敗する——CDN 側設定を確認 |
| Timing-Allow-Origin | SRI とは無関係だが CDN パフォーマンス計測用 |
| Cache-Control | 長期キャッシュ + 固定 URL は SRI と相性良い |
セキュリティヘッダー一覧 と合わせ、レスポンスヘッダーと HTML 属性の両面から攻撃面を狭める。
Next.js / Vite での CDN 読み込みパターン
フレームワーク経由で CDN を使う場合も、最終的に HTML に出力される <script> に integrity が載っているかを確認する。next/script の strategy や Vite の index.html 直書きは、ビルド後の HTML を grep するのが確実だ。
next/script で integrity を指定
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja">
<body>
{children}
<Script
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/analytics.min.js"
integrity="sha384-ANALYTICS_HASH_FROM_OPENSSL"
crossOrigin="anonymous"
strategy="afterInteractive"
/>
</body>
</html>
);
}
crossOrigin は React では camelCase だ。next/script はサーバー Components からそのまま integrity を渡せる。App Router では CSP nonce ミドルウェア と併用し、自前バンドル script に nonce、CDN vendor に integrity という住み分けが多い。
Playwright による E2E 検証
デプロイ後に SRI が効いているかを自動テストする例だ。Console の integrity エラーを監視する。
// tests/sri.spec.ts
import { test, expect } from '@playwright/test';
test('CDN scripts load without integrity errors', async ({ page }) => {
const integrityErrors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error' && msg.text().includes('integrity')) {
integrityErrors.push(msg.text());
}
});
await page.goto('https://staging.example.com/');
await page.waitForLoadState('networkidle');
expect(integrityErrors).toEqual([]);
});
ステージングで CDN バージョンを上げた PR では、このテストが hash 更新漏れを検知する。本番 URL ではなく staging を対象にし、テスト用データで vendor script が実際に読み込まれるページを選ぶ。
関連記事
この記事は セキュリティ テーマの一環です。あわせて読むと理解が深まる関連記事をまとめました。
| トピック | 記事 |
|---|---|
| CSRF対策の実装ガイド | CSRF対策の実装ガイド|SameSite・Synchronizer Token・Laravel/Expressコード |
| Double Submit Cookie完全実装 | Double Submit Cookie完全実装|Synchronizer Token比較・SPA+JWT・サブドメイン攻撃 |
| セキュアなCookie設定ガイド | セキュアなCookie設定ガイド|HttpOnly・Secure・SameSiteの使い分けとCSRF対策 |
| CORSエラーの対処法 | CORSエラーの対処法|「Access-Control-Allow-Origin」で解決する |
| CORSプリフライトリクエスト | CORSプリフライトリクエスト|OPTIONSが飛ぶ条件と正しい応答 |
| ローカル開発でCORSエラーが出る原因と3つの回避策 | ローカル開発でCORSエラーが出る原因と3つの回避策 |
| OAuth2 PKCE SPA実装ガイド | OAuth2 PKCE SPA実装ガイド|Authorization Code + S256 + BFFパターン |
| Passkeys・WebAuthn実装の基礎 | Passkeys・WebAuthn実装の基礎|FIDO2・Resident Key・@simplewebauthn/server |
| JWTのデバッグ方法 | JWTのデバッグ方法|トークンの中身を安全に確認する |
| JWTの中身を確認する方法 | JWTの中身を確認する方法|改ざんが検知される仕組み |
| JWKSとJWT鍵ローテーション運用ガイド | JWKSとJWT鍵ローテーション運用ガイド|kid検証・ゼロダウンタイム・Node.js実装 |
| HSTS Preload設定手順 | HSTS Preload設定手順|nginx・Cloudflare実装とhstspreload.org申請ガイド |
まとめ
Subresource Integrity は、CDN・npm ミラー・サードパーティ script 依存を持つ現代 Web アプリにおいて、ソフトウェア供給チェーンの改ざんをブラウザ側で止めるための標準機能だ。
実装の核心は次の三点に集約される。
- integrity に sha384 ベース64 ハッシュを書く
- crossorigin=“anonymous” を必ず付ける
- バージョン固定 URL と CI 自動更新 で運用する
検証失敗時のフォールバック、CSP との整合、XSS 防御 としての位置づけまで設計すれば、可用性を維持しつつサプライチェーンリスクを現実的な水準まで下げられる。2026 年以降も CDN compromise のニュースは続くだろう——中身を固定する SRI は、そのニュースが自社ユーザーのセッション窃取に繋がらないための、コスト対効果の高い一手だ。