使用子資源完整性、CORS 和參照網址政策保護外部子資源

從第三方 CDN 或外部來源載入指令碼、樣式表、字型或圖片時,請使用子資源完整性 (HTML 中的 integrity 和 CSS url() 中的 integrity()) 驗證其密碼編譯完整性、設定跨來源憑證處理 (crossorigin 和 cross-origin()),並限制傳出 Referer 標頭 (referrerpolicy 和 referrer-policy())。如要瞭解文件層級內容安全政策和外連連線許可清單,請參閱「網路安全性」和「使用連線許可清單限制外連網路連線」。

1. 為不可變動的資產產生密碼編譯完整性摘要

子資源完整性 (SRI) 可確保瀏覽器只在外部資產的位元組與應用程式宣告的密碼雜湊相符時,才會執行或算繪該資產。

  • 在建構或發布時計算摘要,絕不使用即時不受信任的回應:在建構或發布步驟中,從已知良好的本機資產位元組產生完整性雜湊 (或複製套件作者發布的官方摘要),並將其硬式編碼到 HTML、CSS 或 JavaScript 中。切勿在執行階段於瀏覽器中擷取第三方網址,以計算其 integrity 雜湊,因為這會信任遠端伺服器傳回的任何位元組,並破壞 SRI。
  • 預設使用 sha384-:使用 sha384- (建議預設)、sha512- 或 sha256-,將完整性中繼資料格式設為 <algorithm>-<base64-digest>。
  • 將外部子資源固定至不可變更的版本化網址:一律使用不可變更、版本固定或內容定址的網址 (例如 https://cdn.example.com/ui@2.4.0/app.min.js) 參照第三方素材資源,而非可變更的浮動網址 (latest/app.min.js),以免網站受到供應鏈變更影響,並在遠端檔案更新時破壞 SRI。如果必須載入會就地更新的可變動第三方指令碼 (例如供應商數據分析代碼),請省略該網址上的 integrity,並改用內容安全政策 (strict-dynamic/nonce) 或 iframe 沙箱限制。
  • 當端點可能提供多個有效酬載時,請以空格分隔多個摘要:所有支援 SRI 的瀏覽器都會實作 sha256-、sha384- 和 sha512-。如果網址可能合法傳回多個已知良好的酬載之一 (例如在 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 的承諾):

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 控制 CSS 載入資源的擷取模式、參照網址標頭和子資源完整性驗證,包括 background-image、content、mask-image、border-image-source、filter、@font-face src 和 @import 規則:

修飾符函式 引數語法和允許的值 目的
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(...) 內的網址是引用的 <string> 時,要求修飾符才有效 (未引用的網址無法使用修飾符)。
  • 每個 url() 最多只能指定一種修飾符類型:您可以在 url("..." ...) 中以任何順序組合 cross-origin()、referrer-policy() 和 integrity(),但重複兩次相同的修飾符類型 (例如兩個 integrity() 修飾符) 是無效的。如要提供多個雜湊,請在單一 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 模式 (直接封鎖跨源網址)。加入 cross-origin(anonymous) (或 cross-origin(use-credentials)) 可確保每個 CSS 屬性的 CORS 驗證一致。
  • 將修飾符放在 @import 和 @font-face 規則的 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 時,建議使用 HTML <link rel="stylesheet">,而非 CSS @import:由於 <link rel="stylesheet"> 上的 subresource-integrity 和 referrer-policy 廣泛適用於所有瀏覽器,因此只要必須在每個瀏覽器中強制執行完整性驗證,就應使用 <link rel="stylesheet" href="..." integrity="sha384-..." crossorigin="anonymous" referrerpolicy="no-referrer"> 載入外部樣式表,而非 @import。
  2. 依賴步驟 2 的文件層級 Referrer-Policy 標頭或 <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() (適用於支援這兩個修飾符,但不支援 integrity() 的瀏覽器),最後宣告包含 integrity("sha384-...") 的完整宣告:

    • 請勿將未經驗證的 url() 放在同一個以半形逗號分隔的 @font-face src 清單中:在單一 src 描述元中寫入 src: url("font.woff2" cross-origin(anonymous) integrity("sha384-...")), url("font.woff2"); 會在執行階段略過子資源完整性。如果 CDN 提供的字型遭到竄改,導致 integrity() 檢查失敗,瀏覽器會將其視為載入失敗,並自動改用第二個以半形逗號分隔的 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 };
}