Sécuriser les sous-ressources externes avec l'intégrité des sous-ressources, CORS et la règle de provenance

Lorsque vous chargez des scripts, des feuilles de style, des polices ou des images à partir de CDN tiers ou d'origines externes, vérifiez leur intégrité cryptographique avec l'intégrité des sous-ressources (integrity en HTML et integrity() en CSS url()), configurez la gestion des identifiants interorigines (crossorigin et cross-origin()) et limitez les en-têtes Referer sortants (referrerpolicy et referrer-policy()). Pour en savoir plus sur la Content Security Policy au niveau du document et les listes d'autorisation de connexions sortantes, consultez Sécurité Web et Limiter les connexions réseau sortantes avec des listes d'autorisation de connexions.

1. Générer des condensés d'intégrité cryptographique pour les assets immuables

L'intégrité des sous-ressources (SRI, Subresource Integrity) garantit que le navigateur n'exécute ou n'affiche un élément externe que si ses octets correspondent au hachage cryptographique déclaré par votre application.

  • Calculez les condensés au moment de la compilation ou de la publication, jamais à partir de réponses non fiables en direct : générez des hachages d'intégrité lors de l'étape de compilation ou de publication à partir d'octets d'éléments locaux connus (ou copiez le condensé officiel publié par l'auteur du package) et codez-les en dur dans votre code HTML, CSS ou JavaScript. Ne récupérez jamais une URL tierce dans le navigateur au moment de l'exécution pour calculer son propre hachage integrity, qui fait confiance aux octets renvoyés par le serveur distant et annule l'effet de la SRI.
  • Utiliser sha384- par défaut : formatez les métadonnées d'intégrité en tant que <algorithm>-<base64-digest> à l'aide de sha384- (valeur par défaut recommandée), sha512- ou sha256-.
  • Épinglez les sous-ressources externes à des URL immuables et versionnées : référencez toujours les éléments tiers à l'aide d'URL immuables, versionnées ou adressées au contenu (comme https://cdn.example.com/ui@2.4.0/app.min.js) plutôt que d'URL flottantes modifiables (latest/app.min.js), qui exposent votre site à des modifications silencieuses de la chaîne d'approvisionnement et interrompent le SRI dès que le fichier distant est mis à jour. Si vous devez charger un script tiers modifiable qui se met à jour sur place (comme un tag d'analyse de fournisseur), omettez integrity sur cette URL et limitez-le plutôt avec Content Security Policy (strict-dynamic/nonces) ou le sandboxing d'iframe.
  • Séparez plusieurs condensés par un espace lorsqu'un point de terminaison peut diffuser plusieurs charges utiles valides : tous les navigateurs compatibles avec le SRI implémentent sha256-, sha384- et sha512-. Si une URL peut légitimement renvoyer l'une des charges utiles connues (par exemple, lors d'un test A/B, d'un déploiement CDN par étapes ou d'un build négocié par le contenu), listez plusieurs condensés du même algorithme séparés par un espace (par exemple, integrity="sha384-variantA... sha384-variantB..."). Le navigateur accepte la réponse si elle correspond à n'importe quel condensé listé de l'algorithme le plus puissant présent.
  • Diffusez des sous-ressources interorigines avec des en-têtes CORS : les navigateurs ne peuvent vérifier l'intégrité d'une réponse interorigine que lorsqu'elle est lisible par CORS (Access-Control-Allow-Origin: * pour les requêtes anonymes ou l'origine exacte de la requête avec Access-Control-Allow-Credentials: true pour les requêtes authentifiées).
# 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. Scripts HTML, feuilles de style et préchargements sécurisés

Définissez un en-tête de réponse HTTP Referrer-Policy: strict-origin-when-cross-origin au niveau du document (ou incluez <meta name="referrer" content="strict-origin-when-cross-origin"> dans <head>) comme valeur par défaut de référence. Ainsi, toute sous-ressource ou url() CSS qui omet un modificateur de provenance par requête supprime toujours les paramètres de chemin d'accès et de requête sensibles des requêtes interorigines.

Ensuite, sur les éléments externes <script>, <link rel="stylesheet">, <link rel="preload"> et <link rel="modulepreload">, définissez toujours integrity, crossorigin et referrerpolicy ensemble :

  • integrity="sha384-..." : bloque l'exécution ou l'application de la ressource si les octets de réponse ne correspondent pas au condensé attendu (pour un <link rel="stylesheet"> bloqué, linkElement.sheet reste null et un événement error se déclenche).
  • crossorigin="anonymous" (valeur par défaut recommandée pour les éléments CDN publics) ou crossorigin="use-credentials" : obligatoire chaque fois que integrity est défini sur une ressource interorigine. Si vous omettez crossorigin, le navigateur récupère la ressource en mode no-cors (ce qui produit une réponse opaque qui ne peut pas être validée par hachage) et bloque la ressource.
  • referrerpolicy="no-referrer" (valeur par défaut recommandée pour les CDN tiers) ou referrerpolicy="strict-origin-when-cross-origin" : remplace la règle de provenance du document pour empêcher la fuite d'informations sur l'origine ou le chemin d'accès dans l'en-tête de requête Referer vers l'hôte tiers.
<!-- 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>

Lorsque vous récupérez des sous-ressources externes de manière programmatique avec fetch(), transmettez les options integrity, mode, credentials et referrerPolicy correspondantes (une non-concordance de condensé rejette la promesse avec 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. Sécurisez les images, les polices et les feuilles de style importées CSS avec les modificateurs url()

Les modificateurs de requête CSS url() (cross-origin(), referrer-policy() et integrity()) contrôlent le mode de récupération, l'en-tête de provenance et la validation de l'intégrité des sous-ressources des ressources chargées par CSS, y compris les règles background-image, content, mask-image, border-image-source, filter, @font-face src et @import, directement à partir de CSS :

Fonction de modificateur Syntaxe des arguments et valeurs autorisées Objectif
cross-origin(<ident>) Mot clé sans guillemets : anonymous (valeur par défaut recommandée) ou use-credentials Récupère la ressource CSS à l'aide de CORS et contrôle si les identifiants sont envoyés. Incluez toujours cet attribut avec integrity() sur les ressources CSS interorigines.
referrer-policy(<ident>) Mot clé sans guillemets : no-referrer (valeur par défaut recommandée pour les assets tiers), strict-origin-when-cross-origin, origin, same-origin, strict-origin, origin-when-cross-origin, no-referrer-when-downgrade ou unsafe-url Remplace la règle de provenance du document pour cette requête CSS spécifique.
integrity("<string>") Chaîne entre guillemets : "sha384-<base64>" (ou condensés séparés par des espaces "sha384-... sha384-...") Vérifie la ressource CSS récupérée par rapport au condensé cryptographique attendu avant de l'afficher ou de l'appliquer.

Suivez ces règles de syntaxe et de sécurité CSS lorsque vous créez des modificateurs url() :

  • Mettez la chaîne d'URL entre guillemets dans url("...") : les modificateurs de requête ne sont valides que lorsque l'URL dans url(...) est une <string> entre guillemets (les URL sans guillemets ne peuvent pas utiliser de modificateurs).
  • Spécifiez chaque type de modificateur au maximum une fois par url() : vous pouvez combiner cross-origin(), referrer-policy() et integrity() dans n'importe quel ordre dans url("..." ...), mais répéter deux fois le même type de modificateur (par exemple, deux modificateurs integrity()) n'est pas valide. Pour fournir plusieurs hachages, séparez-les par un espace dans une seule chaîne integrity("sha384-... sha384-...").
  • Associez toujours integrity() interorigine à cross-origin(anonymous) : alors que @font-face src et mask-image récupèrent déjà en mode cors anonyme par défaut, background-image, content, border-image-source et @import sont définis par défaut sur le mode no-cors (produisant une réponse opaque qui ne peut pas être vérifiée par hachage) et le filter SVG externe est défini par défaut sur le mode same-origin (bloquant complètement les URL interorigines). L'inclusion de cross-origin(anonymous) (ou cross-origin(use-credentials)) garantit une vérification CORS cohérente pour chaque propriété CSS.
  • Placer les modificateurs dans url("...") dans les règles @import et @font-face : conservez cross-origin(), referrer-policy() et integrity() dans url("..." ...), tout en conservant les descripteurs de police tels que format("woff2") et tech(...) en dehors de 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")
  );
}

Stratégies de remplacement

Si votre cible Baseline n'est pas encore compatible avec les modificateurs de requête CSS url() (cross-origin(), referrer-policy() ou integrity()), combinez des règles au niveau du document avec des déclarations CSS en cascade pour que les sous-ressources restent fonctionnelles et protégées :

  1. Privilégiez le code HTML <link rel="stylesheet"> au code CSS @import lorsque l'intégrité des sous-ressources (SRI) est requise sur plusieurs navigateurs : étant donné que subresource-integrity et referrer-policy sur <link rel="stylesheet"> sont largement disponibles sur tous les navigateurs, chargez les feuilles de style externes avec <link rel="stylesheet" href="..." integrity="sha384-..." crossorigin="anonymous" referrerpolicy="no-referrer"> au lieu de @import chaque fois que la validation de l'intégrité doit être appliquée dans chaque navigateur.
  2. Utilisez l'en-tête Referrer-Policy ou l'élément <meta name="referrer"> au niveau du document de l'étape 2 : dans les navigateurs qui n'acceptent pas referrer-policy() dans le CSS url(), l'en-tête Referrer-Policy: strict-origin-when-cross-origin ou l'élément <meta name="referrer" content="strict-origin-when-cross-origin"> au niveau du document supprime automatiquement les paramètres de chemin d'accès et les paramètres de requête sensibles des requêtes CSS interorigines.
  3. Utilisez des déclarations CSS distinctes en cascade pour l'amélioration progressive : les navigateurs qui ne reconnaissent pas un modificateur dans url("..." ...) ignorent l'intégralité de la déclaration au moment de l'analyse CSS. Déclarez d'abord un url("...") simple, suivi d'une déclaration cross-origin() et referrer-policy() (pour les navigateurs qui acceptent ces deux modificateurs, mais pas integrity()), puis de la déclaration complète incluant integrity("sha384-...") :

    • Ne placez jamais un url() non validé dans la même liste @font-face src séparée par une virgule : l'écriture de src: url("font.woff2" cross-origin(anonymous) integrity("sha384-...")), url("font.woff2"); dans un seul descripteur src contourne l'intégrité des sous-ressources au moment de l'exécution. Si un CDN diffuse une police falsifiée qui échoue à la vérification integrity(), le navigateur la traite comme un échec de chargement et revient automatiquement au deuxième url("font.woff2") séparé par une virgule. L'utilisation de déclarations distinctes src: garantit que les navigateurs compatibles écrasent la déclaration non validée au moment de l'analyse CSS et bloquent les polices falsifiées au moment de l'exécution.
    @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. Détectez la compatibilité du modificateur CSS url() dans JavaScript avec 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 };
}