Beveilig externe subbronnen met Subresource Integrity, CORS en Referrer Policy

Als je scripts, stylesheets, lettertypen of afbeeldingen laadt van CDN's van derden of externe bronnen, controleer je de cryptografische integriteit met Subresource Integrity (integrity in HTML en integrity() in CSS url()), stel je de verwerking van cross-origin-inloggegevens in (crossorigin en cross-origin()) en beperk je uitgaande Referer-headers (referrerpolicy en referrer-policy()). Ga naar Webbeveiliging en Uitgaande netwerkverbindingen beperken met toelatingslijsten voor verbindingen voor Content Security Policy op documentniveau en toelatingslijsten voor uitgaande verbindingen.

1. Cryptografische integriteitsdigests genereren voor onveranderbare items

Subresource Integrity (SRI) zorgt ervoor dat de browser een externe component alleen uitvoert of rendert als de bytes overeenkomen met de cryptografische hash-waarde die door je app is gedeclareerd.

  • Bereken digests op het moment van build of release, nooit op basis van live niet-vertrouwde reacties: Genereer integriteitshashes tijdens de build- of releasestap op basis van bekende goede lokale assetbytes (of kopieer de officiële digest die is gepubliceerd door de auteur van het pakket) en hardcodeer deze in je HTML, CSS of JavaScript. Haal nooit een URL van derden op in de browser tijdens de runtime om de eigen integrity-hash te berekenen. Hierdoor worden de bytes die de externe server retourneert vertrouwd en wordt SRI omzeild.
  • sha384- standaard gebruiken: Maak de integriteitsmetadata op als <algorithm>-<base64-digest> met sha384- (aanbevolen standaard), sha512- of sha256-.
  • Zet externe subbronnen vast aan onveranderlijke URL's met versies: Verwijs altijd naar componenten van derden met onveranderlijke URL's met versies of contentgeadresseerde URL's (zoals https://cdn.example.com/ui@2.4.0/app.min.js) in plaats van veranderlijke zwevende URL's (latest/app.min.js). Deze stellen je site bloot aan ongemerkte wijzigingen in de toeleveringsketen en verbreken SRI zodra het externe bestand wordt geüpdatet. Als je een veranderlijk script van derden moet laden dat ter plekke wordt geüpdatet (zoals een analysetag van een leverancier), laat je integrity weg in die URL en beperk je deze met Content Security Policy (strict-dynamic/nonces) of iframe-sandboxing.
  • Scheid meerdere digests met een spatie als een eindpunt meerdere geldige payloads kan leveren: Alle browsers die SRI ondersteunen, implementeren sha256-, sha384- en sha512-. Als een URL rechtmatig een van de verschillende bekende goede payloads kan retourneren (zoals tijdens een A/B-test, gefaseerde CDN-implementatie of build waarover is onderhandeld over content), vermeld je meerdere samenvattingen van hetzelfde algoritme, gescheiden door witruimte (bijvoorbeeld integrity="sha384-variantA... sha384-variantB..."). De browser accepteert de reactie als deze overeenkomt met een vermelde samenvatting van het sterkste aanwezige algoritme.
  • Cross-origin subresources leveren met CORS-headers: Browsers kunnen de integriteit van een cross-origin reactie alleen verifiëren als deze CORS-leesbaar is (Access-Control-Allow-Origin: * voor anonieme verzoeken of de exacte bron van het verzoek met Access-Control-Allow-Credentials: true voor verzoeken met inloggegevens).
# 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. Beveiligde HTML-scripts, stylesheets en preloads

Stel een HTTP-reactieheader Referrer-Policy: strict-origin-when-cross-origin op documentniveau in (of neem <meta name="referrer" content="strict-origin-when-cross-origin"> op in <head>) als basisstandaard, zodat elke subbron of CSS url() die een verwijzingsmodifier per verzoek weglaat, nog steeds gevoelige pad- en queryparameters verwijdert bij cross-origin-verzoeken.

Stel daarna voor externe <script>-, <link rel="stylesheet">-, <link rel="preload">- en <link rel="modulepreload">-elementen altijd integrity, crossorigin en referrerpolicy samen in:

  • integrity="sha384-...": Blokkeert de uitvoering of toepassing van de bron als de reactiebytes niet overeenkomen met de verwachte digest (voor een geblokkeerde <link rel="stylesheet"> blijft linkElement.sheet null en wordt een error-gebeurtenis geactiveerd).
  • crossorigin="anonymous" (aanbevolen standaard voor openbare CDN-items) of crossorigin="use-credentials": Vereist als integrity is ingesteld voor een cross-origin-resource. Als je crossorigin weglaat, haalt de browser de bron op in de no-cors-modus (wat een ondoorzichtige reactie oplevert die niet kan worden geverifieerd met een hash) en blokkeert de bron.
  • referrerpolicy="no-referrer" (aanbevolen standaardinstelling voor CDN's van derden) of referrerpolicy="strict-origin-when-cross-origin": Overschrijft het documentverwijzingsbeleid om te voorkomen dat er oorsprong- of padinformatie in de Referer-verzoekheader naar de host van derden wordt gelekt.
<!-- 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>

Als je externe subbronnen programmatisch ophaalt met fetch(), geef je de overeenkomende opties integrity, mode, credentials en referrerPolicy door (een digest-mismatch wijst de promise af met 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. Beveilig CSS-afbeeldingen, lettertypen en geïmporteerde stylesheets met url()-modifiers

CSS url()-verzoekmodifiers (cross-origin(), referrer-policy() en integrity()) bepalen de ophaalmodus, de referrer-header en de Subresource Integrity-verificatie van door CSS geladen bronnen, waaronder de regels background-image, content, mask-image, border-image-source, filter, @font-face src en @import, rechtstreeks vanuit CSS:

Functie voor modificatie Argumentsyntaxis en toegestane waarden Doel
cross-origin(<ident>) Zoekwoord zonder aanhalingstekens: anonymous (aanbevolen standaard) of use-credentials Haalt de CSS-bron op met CORS en bepaalt of inloggegevens worden gestuurd. Voeg dit altijd toe naast integrity() in cross-origin CSS-bronnen.
referrer-policy(<ident>) Zoekwoord zonder aanhalingstekens: no-referrer (aanbevolen standaard voor componenten van derden), strict-origin-when-cross-origin, origin, same-origin, strict-origin, origin-when-cross-origin, no-referrer-when-downgrade of unsafe-url Overschrijft het documentverwijzingsbeleid voor dit specifieke CSS-verzoek.
integrity("<string>") Tekenreeks tussen aanhalingstekens: "sha384-<base64>" (of door spaties gescheiden overzichten "sha384-... sha384-...") Verifieert de opgehaalde CSS-bron met de verwachte cryptografische digest voordat deze wordt gerenderd of toegepast.

Volg deze CSS-syntaxis- en beveiligingsregels als je url()-modifiers maakt:

  • Plaats de URL-tekenreeks tussen aanhalingstekens in url("..."): Verzoekmodifiers zijn alleen geldig als de URL in url(...) een <string> tussen aanhalingstekens is (URL's zonder aanhalingstekens kunnen geen modifiers gebruiken).
  • Voer elk type modifier maximaal één keer per url() in: U kunt cross-origin(), referrer-policy() en integrity() in elke volgorde combineren binnen url("..." ...), maar als u hetzelfde type modifier 2 keer herhaalt (zoals 2 integrity()-modifiers), is dit ongeldig. Als je meerdere hashes wilt invoeren, scheid je ze met een spatie in één integrity("sha384-... sha384-...")-tekenreeks.
  • Koppel cross-origin integrity() altijd aan cross-origin(anonymous): Hoewel @font-face src en mask-image standaard al worden opgehaald in de anonieme cors-modus, worden background-image, content, border-image-source en @import standaard ingesteld op de no-cors-modus (wat een ondoorzichtige reactie oplevert die niet kan worden geverifieerd met een hash) en wordt externe SVG filter standaard ingesteld op de same-origin-modus (waardoor cross-origin URL's direct worden geblokkeerd). Als je cross-origin(anonymous) (of cross-origin(use-credentials)) opneemt, zorg je voor consistente CORS-verificatie voor elke CSS-property.
  • Plaats modifiers binnen url("...") in @import- en @font-face-regels: Houd cross-origin(), referrer-policy() en integrity() binnen url("..." ...) en houd lettertypebeschrijvingen zoals format("woff2") en tech(...) buiten 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")
  );
}

Reservestrategieën

Als je Baseline-doel nog geen CSS url()-verzoekmodifiers (cross-origin(), referrer-policy() of integrity()) ondersteunt, combineer je beleid op documentniveau met trapsgewijze CSS-declaraties, zodat subbronnen functioneel en beschermd blijven:

  1. Geef de voorkeur aan HTML <link rel="stylesheet"> boven CSS @import als SRI voor meerdere browsers is vereist: Omdat subresource-integrity en referrer-policy op <link rel="stylesheet"> breed beschikbaar zijn in alle browsers, laad je externe stylesheets met <link rel="stylesheet" href="..." integrity="sha384-..." crossorigin="anonymous" referrerpolicy="no-referrer"> in plaats van @import als integriteitsverificatie moet worden afgedwongen in elke browser.
  2. Vertrouw op de Referrer-Policy-header of <meta name="referrer"> op documentniveau uit stap 2: In browsers die referrer-policy() niet ondersteunen in CSS url(), verwijdert de Referrer-Policy: strict-origin-when-cross-origin-header of het <meta name="referrer" content="strict-origin-when-cross-origin">-element op documentniveau automatisch gevoelige pad- en queryparameters in cross-origin CSS-verzoeken.
  3. Cascadeer afzonderlijke CSS-declaraties voor progressive enhancement: Browsers die een modifier in url("..." ...) niet herkennen, negeren de hele declaratie tijdens de CSS-parse. Declareer eerst een gewone url("..."), gevolgd door een cross-origin()- en referrer-policy()-declaratie (voor browsers die deze 2 modifiers ondersteunen, maar niet integrity()), gevolgd door de volledige declaratie inclusief integrity("sha384-..."):

    • Zet nooit een niet-geverifieerde url() in dezelfde door komma's gescheiden @font-face src-lijst: Als je src: url("font.woff2" cross-origin(anonymous) integrity("sha384-...")), url("font.woff2"); in één src-descriptor schrijft, wordt de integriteit van subbronnen tijdens de runtime omzeild. Als een CDN een gemanipuleerd lettertype levert dat de integrity()-check niet doorstaat, behandelt de browser dit als een laadfout en valt deze automatisch terug op de 2e door komma's gescheiden url("font.woff2"). Als je afzonderlijke src:-declaraties gebruikt, overschrijven ondersteunende browsers de niet-geverifieerde declaratie tijdens de CSS-parse en blokkeren ze gemanipuleerde lettertypen tijdens de runtime.
    @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. Functiedetectie van ondersteuning voor de CSS-modifier url() in JavaScript met CSS.supports():

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 };
}