Zabezpieczanie zewnętrznych zasobów podrzędnych za pomocą integralności zasobów podrzędnych, CORS i zasad dotyczących stron odsyłających

Podczas wczytywania skryptów, arkuszy stylów, czcionek lub obrazów z sieci CDN innych firm lub źródeł zewnętrznych sprawdź ich integralność kryptograficzną za pomocą integralności zasobów podrzędnych (integrity w HTML i integrity() w CSS url()), skonfiguruj obsługę danych logowania ze współdzieleniem (crossorigin i cross-origin()) i ogranicz wychodzące nagłówki Referer (referrerpolicy i referrer-policy()). Informacje o (standard) Content Security Policy na poziomie dokumentu i listach dozwolonych połączeń wychodzących znajdziesz w artykułach o bezpieczeństwie w internecie i ograniczaniu wychodzących połączeń sieciowych za pomocą list dozwolonych połączeń.

1. Generowanie kryptograficznych podsumowań integralności dla zasobów niezmiennych

Subresource Integrity (SRI) zapewnia, że przeglądarka wykonuje lub renderuje zasób zewnętrzny tylko wtedy, gdy jego bajty są zgodne z hashem kryptograficznym zadeklarowanym przez aplikację.

  • Obliczaj skróty podczas kompilacji lub publikowania, nigdy na podstawie niezweryfikowanych odpowiedzi na żywo: generuj skróty integralności podczas kompilacji lub publikowania na podstawie znanych, prawidłowych bajtów zasobów lokalnych (lub skopiuj oficjalny skrót opublikowany przez autora pakietu) i zakoduj je na stałe w kodzie HTML, CSS lub JavaScript. Nigdy nie pobieraj adresu URL innej firmy w przeglądarce w czasie działania, aby obliczyć własny skrót integrity, który ufa wszystkim bajtom zwracanym przez serwer zdalny i uniemożliwia działanie SRI.
  • Domyślnie używaj sha384-: formatuj metadane integralności jako <algorithm>-<base64-digest>, używając sha384- (zalecane ustawienie domyślne), sha512- lub sha256-.
  • Przypnij zewnętrzne zasoby podrzędne do niezmiennych adresów URL z numerem wersji: zawsze odwołuj się do zasobów innych firm, używając niezmiennych adresów URL z numerem wersji lub adresów URL z zawartością (np. https://cdn.example.com/ui@2.4.0/app.min.js), a nie zmiennych adresów URL (latest/app.min.js), które narażają witrynę na niezauważalne zmiany w łańcuchu dostaw i powodują błędy SRI, gdy tylko plik zdalny zostanie zaktualizowany. Jeśli musisz wczytać zmienny skrypt innej firmy, który aktualizuje się na miejscu (np. tag analityczny dostawcy), pomiń integrity w tym adresie URL i zamiast tego ogranicz go za pomocą (standard) Content Security Policy (strict-dynamic / liczby jednorazowe) lub piaskownicy elementu iframe.
  • Oddziel spacjami wiele skrótów, gdy punkt końcowy może obsługiwać wiele prawidłowych ładunków: wszystkie przeglądarki obsługujące SRI implementują sha256-, sha384- i sha512-. Jeśli adres URL może zgodnie z prawem zwracać jeden z kilku znanych prawidłowych ładunków (np. podczas testu A/B, etapowego wdrażania CDN lub kompilacji z negocjacją treści), podaj kilka skrótów tego samego algorytmu rozdzielonych spacjami (np. integrity="sha384-variantA... sha384-variantB..."). Przeglądarka zaakceptuje odpowiedź, jeśli będzie ona zgodna z dowolnym podanym skrótem najsilniejszego obecnego algorytmu.
  • Udostępniaj zasoby podrzędne ze współdzieleniem z nagłówkami CORS: przeglądarki mogą weryfikować integralność odpowiedzi ze współdzieleniem tylko wtedy, gdy jest ona możliwa do odczytania przez CORS (Access-Control-Allow-Origin: * w przypadku żądań anonimowych lub dokładne źródło żądania z Access-Control-Allow-Credentials: true w przypadku żądań z danymi logowania).
# 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. Bezpieczne skrypty HTML, arkusze stylów i wstępne wczytywanie

Ustaw nagłówek odpowiedzi HTTP Referrer-Policy: strict-origin-when-cross-origin na poziomie dokumentu (lub uwzględnij <meta name="referrer" content="strict-origin-when-cross-origin"> w <head>) jako podstawową wartość domyślną, aby każdy zasób podrzędny lub CSS url(), który pomija modyfikator strony odsyłającej dla żądania, nadal usuwał poufne parametry ścieżki i zapytania w żądaniach z innych domen.

Następnie w zewnętrznych elementach <script>, <link rel="stylesheet">, <link rel="preload"> i <link rel="modulepreload"> zawsze ustawiaj razem integrity, crossorigin i referrerpolicy:

  • integrity="sha384-...": blokuje wykonanie lub zastosowanie zasobu, jeśli bajty odpowiedzi nie pasują do oczekiwanego skrótu (w przypadku zablokowanego elementu <link rel="stylesheet"> element linkElement.sheet pozostaje null i wywoływane jest zdarzenie error).
  • crossorigin="anonymous" (zalecana wartość domyślna dla zasobów publicznej sieci CDN) lub crossorigin="use-credentials": wymagane, gdy parametr integrity jest ustawiony w zasobie współdzielonym. Pominięcie crossorigin powoduje, że przeglądarka pobiera zasób w trybie no-cors (co powoduje nieprzezroczystą odpowiedź, której nie można zweryfikować za pomocą haszowania) i blokuje zasób.
  • referrerpolicy="no-referrer" (zalecane ustawienie domyślne dla sieci CDN innych firm) lub referrerpolicy="strict-origin-when-cross-origin": zastępuje zasady dotyczące dokumentu odsyłającego, aby zapobiec wyciekowi informacji o źródle lub ścieżce w nagłówku żądania Referer do hosta zewnętrznego.
<!-- 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>

Podczas pobierania zewnętrznych zasobów podrzędnych za pomocą fetch() przekaż pasujące opcje integrity, mode, credentials i referrerPolicy (niezgodność skrótu odrzuca obietnicę z 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. Zabezpieczanie obrazów, czcionek i importowanych arkuszy stylów CSS za pomocą modyfikatorów url()

Modyfikatory żądań CSS url() (cross-origin(), referrer-policy() i integrity()) kontrolują tryb pobierania, nagłówek strony odsyłającej i weryfikację integralności zasobów podrzędnych w przypadku zasobów wczytywanych przez CSS – w tym reguł background-image, content, mask-image, border-image-source, filter, @font-face src i @import – bezpośrednio z CSS:

Funkcja modyfikatora Składnia argumentów i dozwolone wartości Cel
cross-origin(<ident>) Słowo kluczowe bez cudzysłowu: anonymous (zalecane ustawienie domyślne) lub use-credentials Pobiera zasób CSS za pomocą CORS i kontroluje, czy dane logowania są wysyłane. Zawsze dołączaj razem z integrity() w zasobach CSS współdzielonych między domenami.
referrer-policy(<ident>) Słowo kluczowe bez cudzysłowu: no-referrer (zalecana wartość domyślna w przypadku zasobów zewnętrznych), strict-origin-when-cross-origin, origin, same-origin, strict-origin, origin-when-cross-origin, no-referrer-when-downgrade lub unsafe-url. Zastępuje zasadę dotyczącą strony odsyłającej dokumentu w przypadku tego konkretnego żądania CSS.
integrity("<string>") Ciąg znaków w cudzysłowie: "sha384-<base64>" (lub podsumowania rozdzielone spacjami "sha384-... sha384-...") Przed wyrenderowaniem lub zastosowaniem pobranego zasobu CSS sprawdza go pod kątem oczekiwanego skrótu kryptograficznego.

Podczas tworzenia modyfikatorów url() przestrzegaj tych reguł dotyczących składni CSS i bezpieczeństwa:

  • Umieść ciąg adresu URL w url("..."): modyfikatory żądań są prawidłowe tylko wtedy, gdy adres URL w url(...) jest ujęty w <string> (adresy URL bez cudzysłowów nie mogą używać modyfikatorów).
  • Każdy typ modyfikatora możesz określić maksymalnie raz na url(): możesz łączyć cross-origin(), referrer-policy() i integrity() w dowolnej kolejności w ramach url("..." ...), ale powtórzenie tego samego typu modyfikatora (np. 2 modyfikatorów integrity()) jest nieprawidłowe. Aby podać kilka skrótów, rozdziel je spacjami w jednym ciągu integrity("sha384-... sha384-...").
  • Zawsze łącz współdzielenie integrity() z cross-origin(anonymous): podczas gdy @font-face src i mask-image domyślnie pobierają już w trybie anonimowym cors, background-image, content, border-image-source i @import domyślnie używają trybu no-cors (generując nieprzezroczystą odpowiedź, której nie można zweryfikować za pomocą haszowania), a zewnętrzny SVG filter domyślnie używa trybu same-origin (blokując bezpośrednio adresy URL współdzielenia). Uwzględnienie cross-origin(anonymous) (lub cross-origin(use-credentials)) zapewnia spójną weryfikację CORS we wszystkich właściwościach CSS.
  • Umieść modyfikatory wewnątrz url("...") w regułach @import i @font-face: zachowaj cross-origin(), referrer-policy() i integrity() wewnątrz url("..." ...), a deskryptory czcionek, takie jak format("woff2") i tech(...), na zewnątrz 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")
  );
}

Strategie zastępcze

Jeśli Twój cel Baseline nie obsługuje jeszcze modyfikatorów żądań CSS url() (cross-origin(), referrer-policy() lub integrity()), połącz zasady na poziomie dokumentu z kaskadowymi deklaracjami CSS, aby zasoby podrzędne pozostały funkcjonalne i chronione:

  1. Jeśli wymagana jest integralność zasobów podrzędnych w różnych przeglądarkach, preferuj HTML <link rel="stylesheet"> zamiast CSS @import: ponieważ atrybuty subresource-integrity i referrer-policy w <link rel="stylesheet"> są powszechnie dostępne we wszystkich przeglądarkach, wczytuj zewnętrzne arkusze stylów za pomocą <link rel="stylesheet" href="..." integrity="sha384-..." crossorigin="anonymous" referrerpolicy="no-referrer"> zamiast @import, gdy weryfikacja integralności musi być wymuszana w każdej przeglądarce.
  2. Korzystaj z nagłówka Referrer-Policy lub elementu <meta name="referrer"> na poziomie dokumentu z kroku 2: w przeglądarkach, które nie obsługują referrer-policy() wewnątrz CSS url(), nagłówek Referrer-Policy: strict-origin-when-cross-origin lub element <meta name="referrer" content="strict-origin-when-cross-origin"> na poziomie dokumentu automatycznie usuwa poufne parametry ścieżki i parametry zapytania w żądaniach CSS współdzielenia.
  3. Kaskadowe oddzielne deklaracje CSS na potrzeby progresywnego ulepszania: przeglądarki, które nie rozpoznają modyfikatora wewnątrz url("..." ...), odrzucają całą deklarację podczas analizowania kodu CSS. Najpierw zadeklaruj zwykły url("..."), a następnie deklarację cross-origin() i referrer-policy() (w przypadku przeglądarek, które obsługują te 2 modyfikatory, ale nie integrity()), a potem pełną deklarację, w tym integrity("sha384-..."):

    • Nigdy nie umieszczaj niezweryfikowanego elementu url() na tej samej liście @font-face src rozdzielonej przecinkami: zapisanie src: url("font.woff2" cross-origin(anonymous) integrity("sha384-...")), url("font.woff2"); w jednym deskryptorze src omija integralność zasobów podrzędnych w czasie działania – jeśli sieć CDN udostępnia zmodyfikowaną czcionkę, która nie przechodzi kontroli integrity(), przeglądarka traktuje to jako błąd wczytywania i automatycznie wraca do drugiego elementu url("font.woff2") rozdzielonego przecinkami. Użycie oddzielnych deklaracji src: gwarantuje, że obsługujące je przeglądarki zastąpią niezweryfikowaną deklarację podczas analizowania kodu CSS i zablokują zmodyfikowane czcionki w czasie działania.
    @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. Wykrywanie obsługi modyfikatora CSS url() w języku JavaScript za pomocą 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 };
}