サブリソースの完全性、CORS、リファラー ポリシーを使用して外部サブリソースを保護する

サードパーティの CDN または外部オリジンからスクリプト、スタイルシート、フォント、画像を読み込む場合は、サブリソース完全性(HTML の integrity と CSS url() の integrity())を使用して暗号化の完全性を検証し、クロスオリジンの認証情報の処理(crossorigin と cross-origin())を構成して、送信 Referer ヘッダー(referrerpolicy と referrer-policy())を制限します。ドキュメント レベルのコンテンツ セキュリティ ポリシーとアウトバウンド接続の許可リストについては、ウェブ セキュリティと接続許可リストを使用してアウトバウンド ネットワーク接続を制限するをご覧ください。

1. 不変アセットの暗号化整合性ダイジェストを生成する

サブリソース完全性(SRI)により、ブラウザは、外部アセットのバイトがアプリケーションで宣言された暗号学的ハッシュと一致する場合にのみ、外部アセットを実行またはレンダリングします。

  • ビルド時またはリリース時にダイジェストを計算し、信頼できないライブ レスポンスからは計算しない: ビルドまたはリリース ステップ中に、既知の良好なローカル アセット バイトから整合性ハッシュを生成し(または、パッケージ作成者が公開した公式ダイジェストをコピーし)、HTML、CSS、JavaScript にハードコードします。実行時にブラウザで第三者の URL を取得して独自の integrity ハッシュを計算しないでください。リモートサーバーから返されるバイトを信頼することになり、SRI が無効になります。
  • デフォルトで sha384- を使用する: sha384-(推奨されるデフォルト)、sha512-、または sha256- を使用して、整合性メタデータを <algorithm>-<base64-digest> としてフォーマットします。
  • 外部サブリソースを不変のバージョン付き URL に固定する: サードパーティのアセットを参照する場合は、常に不変のバージョン固定 URL またはコンテンツ アドレス URL(https://cdn.example.com/ui@2.4.0/app.min.js など)を使用します。変更可能なフローティング URL(latest/app.min.js)を使用すると、サイトがサプライ チェーンの変更に気付かないうちにさらされ、リモート ファイルが更新されるとすぐに SRI が破損します。ベンダーの分析タグなど、インプレースで更新される変更可能なサードパーティ スクリプトを読み込む必要がある場合は、その URL で integrity を省略し、代わりにコンテンツ セキュリティ ポリシー(strict-dynamic / nonce)または iframe サンドボックスで制限します。
  • エンドポイントが複数の有効なペイロードを提供する可能性がある場合は、複数のダイジェストをスペースで区切る: SRI をサポートするすべてのブラウザは、sha256-、sha384-、sha512- を実装しています。URL が既知の良好なペイロードのいずれかを正当に返す可能性がある場合(A/B テスト、段階的な CDN ロールアウト、コンテンツ ネゴシエーションされたビルドなど)、同じアルゴリズムの複数のダイジェストを空白で区切ってリストします(例: integrity="sha384-variantA... sha384-variantB...")。ブラウザは、存在する最も強力なアルゴリズムのリストされたダイジェストのいずれかに一致する場合、レスポンスを受け入れます。
  • CORS ヘッダーを使用してクロスオリジン サブリソースを提供する: ブラウザは、クロスオリジン レスポンスが CORS で読み取り可能な場合にのみ、その整合性を検証できます(匿名リクエストの場合は Access-Control-Allow-Origin: *、認証情報付きリクエストの場合は Access-Control-Allow-Credentials: true を含む正確なリクエスト元)。
# Generate a sha384- SRI hash at build time from a known-good local file
printf "sha384-%s\n" "$(openssl dgst -sha384 -binary app.min.js | openssl base64 -A)"
// Build-time / tooling helper (Node.js 19+, Deno, Bun) to compute an SRI string from known-good local bytes
export async function computeIntegrityMetadata(bytes) {
  const rawBytes =
    typeof bytes === 'string' ? new TextEncoder().encode(bytes) : bytes;
  const digestBuffer = await crypto.subtle.digest('SHA-384', rawBytes);
  const binary = Array.from(new Uint8Array(digestBuffer), (b) =>
    String.fromCharCode(b),
  ).join('');
  return `sha384-${btoa(binary)}`;
}

2. 安全な HTML スクリプト、スタイルシート、プリロード

ドキュメント レベルの Referrer-Policy: strict-origin-when-cross-origin HTTP レスポンス ヘッダーを設定(または <head> に <meta name="referrer" content="strict-origin-when-cross-origin"> を含める)して、ベースラインのデフォルトとして、リクエストごとのリファラー修飾子を省略するサブリソースまたは CSS url() が、クロスオリジン リクエストで機密性の高いパスとクエリ パラメータを削除するようにします。

次に、外部の <script>、<link rel="stylesheet">、<link rel="preload">、<link rel="modulepreload"> 要素で、integrity、crossorigin、referrerpolicy を常に一緒に設定します。

  • integrity="sha384-...": レスポンス バイトが想定されるダイジェストと一致しない場合、リソースの実行または適用をブロックします(ブロックされた <link rel="stylesheet"> の場合、linkElement.sheet は null のままで、error イベントが発生します)。
  • crossorigin="anonymous"(一般公開 CDN アセットに推奨されるデフォルト)または crossorigin="use-credentials": クロスオリジン リソースに integrity が設定されている場合は必須です。crossorigin を省略すると、ブラウザは no-cors モードでリソースを取得し(ハッシュ検証できない不透明なレスポンスを生成)、リソースをブロックします。
  • referrerpolicy="no-referrer"(サードパーティ CDN の推奨デフォルト)または referrerpolicy="strict-origin-when-cross-origin": ドキュメントのリファラー ポリシーをオーバーライドして、Referer リクエスト ヘッダーのオリジン情報やパス情報がサードパーティ ホストに漏洩しないようにします。
<!-- Document-level baseline referrer policy in <head> (or via Referrer-Policy HTTP header) -->
<meta name="referrer" content="strict-origin-when-cross-origin" />

<!-- External stylesheet with SRI, anonymous CORS, and no referrer -->
<link
  rel="stylesheet"
  href="https://cdn.example.com/ui@2.4.0/styles.css"
  integrity="sha384-bs0EksOYBbU/g0IAHMXbSyH1Mgw83GKn+EZlSI+VE+zVchXmSbLwhVLrPzsOWnc5"
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
/>

<!-- External script with SRI, anonymous CORS, and no referrer -->
<script
  src="https://cdn.example.com/ui@2.4.0/bundle.min.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
></script>

fetch() を使用して外部サブリソースをプログラムで取得する場合は、一致する integrity、mode、credentials、referrerPolicy オプションを渡します(ダイジェストが一致しない場合、TypeError で promise が拒否されます)。

const response = await fetch('https://cdn.example.com/ui@2.4.0/manifest.json', {
  integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC',
  mode: 'cors',
  credentials: 'omit',
  referrerPolicy: 'no-referrer',
});

3. url() 修飾子を使用して CSS 画像、フォント、インポートされたスタイルシートを保護する

CSS url() リクエスト修飾子(cross-origin()、referrer-policy()、integrity())は、CSS で読み込まれたリソース(background-image、content、mask-image、border-image-source、filter、@font-face src、@import ルールなど)のフェッチモード、リファラー ヘッダー、サブリソース整合性検証を CSS から直接制御します。

修飾子関数 引数の構文と使用できる値 目的
cross-origin(<ident>) 引用符なしのキーワード: anonymous(推奨されるデフォルト)または use-credentials CORS を使用して CSS リソースを取得し、認証情報を送信するかどうかを制御します。クロスオリジン CSS リソースでは、常に integrity() と一緒に含めます。
referrer-policy(<ident>) 引用符なしのキーワード: no-referrer(サードパーティ アセットの推奨デフォルト)、strict-origin-when-cross-origin、origin、same-origin、strict-origin、origin-when-cross-origin、no-referrer-when-downgrade、unsafe-url この特定の CSS リクエストのドキュメント リファラー ポリシーをオーバーライドします。
integrity("<string>") 引用符付き文字列: "sha384-<base64>"(またはスペース区切りのダイジェスト "sha384-... sha384-...") 取得した CSS リソースをレンダリングまたは適用する前に、想定される暗号ダイジェストと照合して検証します。

url() 修飾子を作成する際は、次の CSS 構文とセキュリティ ルールに従ってください。

  • url("...") 内の URL 文字列を引用符で囲む: リクエスト修飾子は、url(...) 内の URL が引用符で囲まれた <string> である場合にのみ有効です(引用符で囲まれていない URL では修飾子を使用できません)。
  • 各修飾子タイプは url() ごとに 1 回だけ指定する: url("..." ...) 内で cross-origin()、referrer-policy()、integrity() を任意の順序で組み合わせることができますが、同じ修飾子タイプを 2 回繰り返す(2 つの integrity() 修飾子など)ことはできません。複数のハッシュを指定するには、1 つの integrity("sha384-... sha384-...") 文字列内でスペースで区切ります。
  • クロスオリジンの integrity() と cross-origin(anonymous) を常にペアにする: @font-face src と mask-image はデフォルトで匿名 cors モードでフェッチされますが、background-image、content、border-image-source、@import はデフォルトで no-cors モード(ハッシュ検証できない不透明なレスポンスを生成)になり、外部 SVG filter はデフォルトで same-origin モード(クロスオリジン URL を完全にブロック)になります。cross-origin(anonymous)(または cross-origin(use-credentials))を含めると、すべての CSS プロパティで一貫した CORS 検証が保証されます。
  • @import ルールと @font-face ルールで、修飾子を url("...") 内の url("...") 内に配置する: cross-origin()、referrer-policy()、integrity() は url("..." ...) 内に配置し、format("woff2") や tech(...) などのフォント記述子は url(...) の外に配置します。
@import url(
  "https://cdn.example.com/theme@1.2.0/tokens.css"
  cross-origin(anonymous)
  referrer-policy(no-referrer)
  integrity("sha384-bs0EksOYBbU/g0IAHMXbSyH1Mgw83GKn+EZlSI+VE+zVchXmSbLwhVLrPzsOWnc5")
);

@font-face {
  font-family: "BrandSans";
  src: url(
    "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
    cross-origin(anonymous)
    referrer-policy(no-referrer)
    integrity("sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC")
  ) format("woff2");
  font-display: swap;
}

.verified-graphic {
  background-image: url(
    "https://cdn.example.com/media@1.0.0/badge.svg"
    cross-origin(anonymous)
    referrer-policy(no-referrer)
    integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91")
  );
}

代替戦略

ベースライン ターゲットが CSS url() リクエスト修飾子(cross-origin()、referrer-policy()、integrity())をまだサポートしていない場合は、ドキュメント レベルのポリシーとカスケード CSS 宣言を組み合わせて、サブリソースが機能し、保護されるようにします。

  1. クロスブラウザ SRI が必要な場合は、CSS @import よりも HTML <link rel="stylesheet"> を優先する: <link rel="stylesheet"> の subresource-integrity と referrer-policy はすべてのブラウザで広く利用できるため、すべてのブラウザで整合性検証を適用する必要がある場合は、@import ではなく <link rel="stylesheet" href="..." integrity="sha384-..." crossorigin="anonymous" referrerpolicy="no-referrer"> を使用して外部スタイルシートを読み込みます。
  2. ドキュメント レベルの Referrer-Policy ヘッダーまたはステップ 2 の <meta name="referrer"> を利用する: CSS url() 内の referrer-policy() をサポートしていないブラウザでは、ドキュメント レベルの Referrer-Policy: strict-origin-when-cross-origin ヘッダーまたは <meta name="referrer" content="strict-origin-when-cross-origin"> 要素が、クロスオリジンの CSS リクエストで機密性の高いパスとクエリ パラメータを自動的に削除します。
  3. プログレッシブ エンハンスメントのために個別の CSS 宣言をカスケードする: url("..." ...) 内の修飾子を認識しないブラウザは、CSS の解析時に宣言全体を破棄します。まず、プレーンな url("...") を宣言し、次に cross-origin() と referrer-policy() の宣言(これら 2 つの修飾子をサポートしているが integrity() はサポートしていないブラウザ用)、最後に integrity("sha384-...") を含む完全な宣言を記述します。

    • 未検証の url() を同じカンマ区切りの @font-face src リストに含めないでください。1 つの src 記述子に src: url("font.woff2" cross-origin(anonymous) integrity("sha384-...")), url("font.woff2"); を記述すると、実行時にサブリソースの整合性がバイパスされます。CDN が integrity() チェックに失敗した改ざんされたフォントを提供すると、ブラウザはそれを読み込みエラーとして扱い、自動的に 2 番目のカンマ区切りの url("font.woff2") にフォールバックします。別々の src: 宣言を使用すると、サポートされているブラウザは CSS の解析時に未検証の宣言を上書きし、実行時に改ざんされたフォントをブロックします。
    @font-face {
      font-family: "BrandSans";
      /* 1. Parse-time fallback for browsers without url() modifier support */
      src: url("https://cdn.example.com/fonts@1.0.0/brand-sans.woff2") format("woff2");
      /* 2. Parse-time fallback for browsers supporting cross-origin() and referrer-policy() */
      src: url(
        "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
      ) format("woff2");
      /* 3. Full protection in browsers supporting cross-origin(), referrer-policy(), and integrity() */
      src: url(
        "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
        integrity("sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC")
      ) format("woff2");
      font-display: swap;
    }
    
    .verified-graphic {
      background-image: url("https://cdn.example.com/media@1.0.0/badge.svg");
      background-image: url(
        "https://cdn.example.com/media@1.0.0/badge.svg"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
      );
      background-image: url(
        "https://cdn.example.com/media@1.0.0/badge.svg"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
        integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91")
      );
    }
    
  4. CSS.supports() を使用して、JavaScript で CSS url() 修飾子のサポートを機能検出する:

export function getCssUrlModifierSupport() {
  const supportsCrossOriginAndReferrer =
    typeof CSS !== 'undefined' &&
    typeof CSS.supports === 'function' &&
    CSS.supports(
      'background-image',
      'url("https://example.com/a.svg" cross-origin(anonymous) referrer-policy(no-referrer))',
    );

  const supportsUrlIntegrity =
    typeof CSS !== 'undefined' &&
    typeof CSS.supports === 'function' &&
    CSS.supports(
      'background-image',
      'url("https://example.com/a.svg" cross-origin(anonymous) referrer-policy(no-referrer) integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91"))',
    );

  return { supportsCrossOriginAndReferrer, supportsUrlIntegrity };
}