Proteger sub-recursos externos com integridade de sub-recursos, CORS e política de referenciador

Ao carregar scripts, folhas de estilo, fontes ou imagens de CDNs de terceiros ou origens externas, verifique a integridade criptográfica deles com a integridade de subrecursos (integrity em HTML e integrity() em CSS url()), configure o processamento de credenciais entre origens (crossorigin e cross-origin()) e restrinja os cabeçalhos Referer de saída (referrerpolicy e referrer-policy()). Para conferir a política de segurança de conteúdo no nível do documento e as listas de permissões de conexão de saída, consulte Segurança da Web e Restringir conexões de rede de saída com listas de permissões de conexão.

1. Gerar resumos de integridade criptográfica para recursos imutáveis

A integridade de sub-recursos (SRI, na sigla em inglês) garante que o navegador só execute ou renderize um recurso externo se os bytes dele corresponderem ao hash criptográfico declarado pelo aplicativo.

  • Calcule resumos no momento do build ou da versão, nunca de respostas dinâmicas não confiáveis: gere hashes de integridade durante a etapa de build ou versão usando bytes de recursos locais conhecidos (ou copie o resumo oficial publicado pelo autor do pacote) e codifique-os no HTML, CSS ou JavaScript. Nunca busque um URL de terceiros no navegador no momento da execução para calcular o próprio hash integrity, que confia em qualquer byte que o servidor remoto retornar e anula a SRI.
  • Usar sha384- por padrão: formate os metadados de integridade como <algorithm>-<base64-digest> usando sha384- (padrão recomendado), sha512- ou sha256-.
  • Fixe subrecursos externos a URLs imutáveis e com controle de versões: sempre faça referência a recursos de terceiros usando URLs imutáveis, com controle de versões ou endereçados por conteúdo (como https://cdn.example.com/ui@2.4.0/app.min.js), em vez de URLs flutuantes mutáveis (latest/app.min.js), que expõem seu site a mudanças silenciosas na cadeia de suprimentos e interrompem a SRI assim que o arquivo remoto é atualizado. Se você precisar carregar um script mutável de terceiros que é atualizado no local (como uma tag de análise de fornecedor), omita integrity nesse URL e restrinja-o com a Política de Segurança de Conteúdo (strict-dynamic / nonces) ou o sandbox de iframe.
  • Separe vários resumos com espaços quando um endpoint puder exibir vários payloads válidos: todos os navegadores compatíveis com SRI implementam sha256-, sha384- e sha512-. Se um URL puder retornar legitimamente um dos vários payloads conhecidos (como durante um teste A/B, um lançamento de CDN em etapas ou um build negociado por conteúdo), liste vários resumos do mesmo algoritmo separados por espaços em branco (por exemplo, integrity="sha384-variantA... sha384-variantB..."). O navegador vai aceitar a resposta se ela corresponder a qualquer resumo listado do algoritmo mais forte presente.
  • Disponibilizar sub-recursos entre origens com cabeçalhos CORS: os navegadores só podem verificar a integridade de uma resposta entre origens quando ela é legível por CORS (Access-Control-Allow-Origin: * para solicitações anônimas ou a origem exata da solicitação com Access-Control-Allow-Credentials: true para solicitações com credenciais).
# 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, folhas de estilo e pré-carregamentos HTML seguros

Defina um cabeçalho de resposta HTTP Referrer-Policy: strict-origin-when-cross-origin no nível do documento (ou inclua <meta name="referrer" content="strict-origin-when-cross-origin"> em <head>) como um padrão de referência para que qualquer subrecurso ou url() de CSS que omita um modificador de referenciador por solicitação ainda remova o caminho sensível e os parâmetros de consulta em solicitações entre origens.

Em seguida, nos elementos externos <script>, <link rel="stylesheet">, <link rel="preload"> e <link rel="modulepreload">, sempre defina integrity, crossorigin e referrerpolicy juntos:

  • integrity="sha384-...": bloqueia a execução ou o aplicativo do recurso se os bytes de resposta não corresponderem ao resumo esperado (para um <link rel="stylesheet"> bloqueado, linkElement.sheet permanece null e um evento error é acionado).
  • crossorigin="anonymous" (padrão recomendado para recursos de CDN públicos) ou crossorigin="use-credentials": obrigatório sempre que integrity é definido em um recurso entre origens. Omitir crossorigin faz com que o navegador busque o recurso no modo no-cors (produzindo uma resposta opaca que não pode ser verificada por hash) e bloqueie o recurso.
  • referrerpolicy="no-referrer" (padrão recomendado para CDNs de terceiros) ou referrerpolicy="strict-origin-when-cross-origin": substitui a política de referenciador do documento para evitar o vazamento de informações de origem ou caminho no cabeçalho da solicitação Referer para o host de terceiros.
<!-- 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>

Ao buscar subrecursos externos de forma programática com fetch(), transmita as opções integrity, mode, credentials e referrerPolicy correspondentes (uma incompatibilidade de resumo rejeita a promessa com 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. Proteger imagens, fontes e folhas de estilo importadas de CSS com modificadores url()

Os modificadores de solicitação CSS url() (cross-origin(), referrer-policy() e integrity()) controlam o modo de busca, o cabeçalho do referenciador e a verificação de integridade de subrecursos de recursos carregados por CSS, incluindo as regras background-image, content, mask-image, border-image-source, filter, @font-face src e @import, diretamente do CSS:

Função modificadora Sintaxe de argumentos e valores permitidos Finalidade
cross-origin(<ident>) Palavra-chave sem aspas: anonymous (padrão recomendado) ou use-credentials Busca o recurso CSS usando CORS e controla se as credenciais são enviadas. Sempre inclua ao lado de integrity() em recursos CSS entre origens.
referrer-policy(<ident>) Palavra-chave sem aspas: no-referrer (padrão recomendado para recursos de terceiros), strict-origin-when-cross-origin, origin, same-origin, strict-origin, origin-when-cross-origin, no-referrer-when-downgrade ou unsafe-url Substitui a política de referenciador de documentos para essa solicitação de CSS específica.
integrity("<string>") String entre aspas: "sha384-<base64>" (ou resumos separados por espaço "sha384-... sha384-...") Verifica o recurso CSS buscado em relação ao resumo criptográfico esperado antes de renderizá-lo ou aplicá-lo.

Siga estas regras de sintaxe e segurança de CSS ao criar modificadores url():

  • Coloque a string do URL entre aspas em url("..."): os modificadores de solicitação só são válidos quando o URL em url(...) é uma <string> entre aspas (URLs sem aspas não podem usar modificadores).
  • Especifique cada tipo de modificador no máximo uma vez por url(): é possível combinar cross-origin(), referrer-policy() e integrity() em qualquer ordem dentro de url("..." ...), mas repetir o mesmo tipo de modificador duas vezes (como dois modificadores integrity()) é inválido. Para fornecer vários hashes, separe-os com espaços dentro de uma única string integrity("sha384-... sha384-...").
  • Sempre use integrity() entre origens com cross-origin(anonymous): embora @font-face src e mask-image já busquem no modo anônimo cors por padrão, background-image, content, border-image-source e @import usam o modo no-cors por padrão (produzindo uma resposta opaca que não pode ser verificada por hash), e o SVG externo filter usa o modo same-origin por padrão (bloqueando URLs entre origens imediatamente). Incluir cross-origin(anonymous) (ou cross-origin(use-credentials)) garante uma verificação de CORS consistente em todas as propriedades CSS.
  • Coloque modificadores dentro de url("...") nas regras @import e @font-face: mantenha cross-origin(), referrer-policy() e integrity() dentro de url("..." ...), mas mantenha descritores de fonte como format("woff2") e tech(...) fora 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")
  );
}

Estratégias de fallback

Se o destino do Baseline ainda não for compatível com os modificadores de solicitação url() do CSS (cross-origin(), referrer-policy() ou integrity()), combine as políticas no nível do documento com declarações CSS em cascata para que os subrecursos permaneçam funcionais e protegidos:

  1. Dê preferência ao HTML <link rel="stylesheet"> em vez do CSS @import quando a SRI entre navegadores for necessária: como subresource-integrity e referrer-policy em <link rel="stylesheet"> estão amplamente disponíveis em todos os navegadores, carregue folhas de estilo externas com <link rel="stylesheet" href="..." integrity="sha384-..." crossorigin="anonymous" referrerpolicy="no-referrer"> em vez de @import sempre que a verificação de integridade precisar ser aplicada em todos os navegadores.
  2. Confie no cabeçalho Referrer-Policy no nível do documento ou em <meta name="referrer"> da Etapa 2: em navegadores que não oferecem suporte a referrer-policy() dentro de CSS url(), o cabeçalho Referrer-Policy: strict-origin-when-cross-origin no nível do documento ou o elemento <meta name="referrer" content="strict-origin-when-cross-origin"> remove automaticamente o caminho sensível e os parâmetros de consulta em solicitações de CSS entre origens.
  3. Cascatear declarações CSS separadas para aprimoramento progressivo: os navegadores que não reconhecem um modificador dentro de url("..." ...) descartam toda a declaração no momento da análise do CSS. Declare um url("...") simples primeiro, seguido por uma declaração cross-origin() e referrer-policy() (para navegadores que oferecem suporte a esses dois modificadores, mas não a integrity()), seguido pela declaração completa, incluindo integrity("sha384-..."):

    • Nunca coloque um url() não verificado na mesma lista @font-face src separada por vírgulas: escrever src: url("font.woff2" cross-origin(anonymous) integrity("sha384-...")), url("font.woff2"); em um único descritor src ignora a integridade de sub-recursos no momento da execução. Se uma CDN exibir uma fonte adulterada que não passe na verificação integrity(), o navegador vai tratar isso como uma falha de carregamento e vai usar automaticamente o segundo url("font.woff2") separado por vírgulas. O uso de declarações src: separadas garante que os navegadores compatíveis substituam a declaração não verificada no momento da análise do CSS e bloqueiem fontes adulteradas no momento da execução.
    @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. Detecte o suporte ao modificador url() do CSS no JavaScript com 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 };
}