Reduza o scripting em vários locais (XSS) com uma Política de Segurança de Conteúdo (CSP) rígida

Lukas Weichselbaum
Lukas Weichselbaum

Compatibilidade com navegadores

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

Origem

O scripting em vários sites (XSS), a capacidade de injetar scripts maliciosos em um app da Web, tem sido uma das maiores vulnerabilidades de segurança da Web por mais de uma década.

A política de segurança de conteúdo (CSP) é uma camada extra de segurança que ajuda a mitigar o XSS. Para configurar um CSP, adicione o cabeçalho HTTP Content-Security-Policy a uma página da Web e defina valores que controlam quais recursos o user agent pode carregar para essa página.

Esta página explica como usar um CSP com base em valores de uso único ou hashes para mitigar XSS, em vez dos CSPs com base em lista de permissões de host usados com frequência, que geralmente deixam a página exposta a XSS porque podem ser ignorados na maioria das configurações.

Termo-chave: um valor de uso único é um número aleatório usado apenas uma vez que pode ser usado para marcar uma tag <script> como confiável.

Termo-chave: uma função hash é uma função matemática que converte um valor de entrada em um valor numérico compactado chamado hash. É possível usar um hash (por exemplo, SHA-256) para marcar uma tag <script> inline como confiável.

Uma política de segurança de conteúdo baseada em valores de uso único ou hashes é chamada de CSP estrita. Quando um aplicativo usa um CSP rigoroso, os invasores que encontram falhas de injeção de HTML geralmente não podem forçar o navegador a executar scripts maliciosos em um documento vulnerável. Isso ocorre porque o CSP estrito só permite scripts hash ou scripts com o valor de uso único correto gerado no servidor. Assim, os invasores não podem executar o script sem saber o valor de uso único correto para uma determinada resposta.

Por que usar um CSP rígido?

Se o site já tiver um CSP semelhante a script-src www.googleapis.com, ele provavelmente não será eficaz contra ataques entre sites. Esse tipo de CSP é chamado de CSP de lista de permissões. Eles exigem muita personalização e podem ser ignorados por invasores.

CSPs rígidos com base em valores de uso único ou hashes criptográficos evitam essas armadilhas.

Estrutura de CSP rigorosa

Uma política de segurança de conteúdo rígida básica usa um dos seguintes cabeçalhos de resposta HTTP:

CSP rigorosa baseada em chave de uso único

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
Como funciona um CSP rigoroso baseado em valor de uso único.

CSP rigorosa baseada em hash

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

As propriedades a seguir tornam um CSP como este "rígido" e, portanto, seguro:

  • Ele usa nonces 'nonce-{RANDOM}' ou hashes 'sha256-{HASHED_INLINE_SCRIPT}' para indicar quais tags <script> o desenvolvedor do site confia para executar no navegador do usuário.
  • Ele define 'strict-dynamic' para reduzir o esforço de implantação de um CSP baseado em nonce ou hash, permitindo automaticamente a execução de scripts criados por um script confiável. Isso também desbloqueia o uso da maioria das bibliotecas e widgets JavaScript de terceiros.
  • Ele não é baseado em listas de permissões de URL, portanto, não sofre com bypasses comuns de CSP.
  • Ele bloqueia scripts inline não confiáveis, como manipuladores de eventos inline ou URIs javascript:.
  • Ele restringe object-src para desativar plug-ins perigosos, como o Flash.
  • Ele restringe base-uri para bloquear a injeção de tags <base>. Isso impede que invasores mudem os locais dos scripts carregados de URLs relativos.

Adotar uma CSP rigorosa

Para adotar um CSP rígido, você precisa:

  1. Decida se o aplicativo precisa definir um CSP baseado em valor de uso único ou hash.
  2. Copie o CSP da seção Estrutura de CSP estrita e defina-o como um cabeçalho de resposta em todo o aplicativo.
  3. Refatore modelos HTML e código do lado do cliente para remover padrões incompatíveis com o CSP.
  4. Implante a CSP.

Você pode usar a auditoria de Práticas recomendadas do Lighthouse (v7.3.0 e mais recentes com a flag --preset=experimental) durante esse processo para verificar se o site tem um CSP e se ele é rigoroso o suficiente para ser eficaz contra XSS.

O Lighthouse
  informa um aviso de que nenhuma CSP foi encontrada no modo restrito.
Se o site não tiver um CSP, o Lighthouse vai mostrar este aviso.

Etapa 1: decidir se você precisa de uma CSP baseada em valor de uso único ou hash

Confira como os dois tipos de CSP rígidos funcionam:

CSP baseado em nonce

Com uma CSP baseada em valor de uso único, você gera um número aleatório no momento da execução, o inclui na CSP e o associa a todas as tags de script na página. Um invasor não pode incluir ou executar um script malicioso na sua página, porque ele precisa adivinhar o número aleatório correto para esse script. Isso só funciona se o número não puder ser adivinhado e for gerado no momento da execução para cada resposta.

Use um CSP baseado em nonce para páginas HTML renderizadas no servidor. Para essas páginas, é possível criar um novo número aleatório para cada resposta.

CSP com base em hash

Para um CSP baseado em hash, o hash de cada tag de script inline é adicionado ao CSP. Cada script tem um hash diferente. Um invasor não pode incluir ou executar um script malicioso na sua página, porque o hash desse script precisa estar no CSP para que ele seja executado.

Use um CSP baseado em hash para páginas HTML servidas de forma estática ou páginas que precisam ser armazenadas em cache. Por exemplo, é possível usar um CSP baseado em hash para aplicativos da Web de página única criados com frameworks como Angular, React ou outros que são servidos de forma estática sem renderização do lado do servidor.

Etapa 2: definir um CSP rígido e preparar seus scripts

Ao definir um CSP, você tem algumas opções:

  • Modo somente relatório (Content-Security-Policy-Report-Only) ou modo de aplicação (Content-Security-Policy). No modo somente relatório, o CSP ainda não bloqueia recursos. Portanto, nada no seu site é interrompido, mas você pode encontrar erros e receber relatórios de tudo o que seria bloqueado. Localmente, quando você configura o CSP, isso não importa muito, porque os dois modos mostram os erros no console do navegador. O modo de aplicação pode ajudar a encontrar recursos que o CSP do rascunho bloqueia, porque bloquear um recurso pode fazer com que a página pareça corrompida. O modo somente para relatórios se torna mais útil mais tarde no processo (consulte a Etapa 5).
  • Cabeçalho ou tag HTML <meta>. Para desenvolvimento local, uma tag <meta> pode ser mais conveniente para ajustar seu CSP e ver rapidamente como ele afeta seu site. No entanto:
    • Mais tarde, ao implantar o CSP na produção, recomendamos configurá-lo como um cabeçalho HTTP.
    • Se você quiser definir o CSP no modo somente relatório, será necessário defini-lo como um cabeçalho, porque as metatags do CSP não oferecem suporte a esse modo.

Opção A: CSP com valor de uso único

Defina o seguinte cabeçalho de resposta HTTP Content-Security-Policy no seu aplicativo:

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Gerar um valor de uso único para o CSP

Um valor de uso único é um número aleatório usado apenas uma vez por carregamento de página. Um CSP baseado em valor de uso único só pode mitigar XSS se os invasores não conseguirem adivinhar o valor de uso único. Um valor de uso único do CSP precisa ser:

  • Um valor aleatório criptograficamente seguro (de preferência com mais de 128 bits de comprimento)
  • Gerado novamente para cada resposta
  • Codificação em Base64

Confira alguns exemplos de como adicionar um valor de uso único do CSP em frameworks do lado do servidor:

const app = express();

app.get('/', function(request, response) {
  // Generate a new random nonce value for every response.
  const nonce = crypto.randomBytes(16).toString("base64");

  // Set the strict nonce-based CSP response header
  const csp = `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`;
  response.set("Content-Security-Policy", csp);

  // Every <script> tag in your application should set the `nonce` attribute to this value.
  response.render(template, { nonce: nonce });
});

Adicionar um atributo nonce aos elementos <script>

Com um CSP baseado em nonce, cada elemento <script> precisa ter um atributo nonce que corresponda ao valor de nonce aleatório especificado no cabeçalho do CSP. Todos os scripts podem ter o mesmo valor de uso único. A primeira etapa é adicionar esses atributos a todos os scripts para que o CSP os permita.

Opção B: cabeçalho de resposta do CSP com base em hash

Defina o seguinte cabeçalho de resposta HTTP Content-Security-Policy no seu aplicativo:

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Para vários scripts inline, a sintaxe é a seguinte: 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'.

Carregar scripts de origem dinamicamente

É possível carregar scripts de terceiros dinamicamente usando um script inline.

Um exemplo de como inline seus scripts.
Permitido pelo CSP
<script>
  var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js'];

  scripts.forEach(function(scriptUrl) {
    var s = document.createElement('script');
    s.src = scriptUrl;
    s.async = false; // to preserve execution order
    document.head.appendChild(s);
  });
</script>
Para executar esse script, calcule o hash do script inline e adicione-o ao cabeçalho de resposta do CSP, substituindo o marcador de posição {HASHED_INLINE_SCRIPT}. Para reduzir a quantidade de hashes, você pode mesclar todos os scripts inline em um único script. Para conferir isso em ação, consulte este exemplo e o código dele.
Bloqueado pela CSP
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
O CSP bloqueia esses scripts porque eles não foram adicionados dinamicamente e não têm o atributo integrity que corresponde a uma origem permitida.

Considerações sobre o carregamento de scripts

O exemplo de script inline adiciona s.async = false para garantir que foo seja executado antes de bar, mesmo que bar seja carregado primeiro. Neste snippet, s.async = false não bloqueia o analisador enquanto os scripts são carregados, porque eles são adicionados dinamicamente. O analisador só é interrompido enquanto os scripts são executados, como acontece com os scripts async. No entanto, com esse snippet, lembre-se do seguinte:

  • Um ou ambos os scripts podem ser executados antes que o download do documento seja concluído. Se você quiser que o documento esteja pronto quando os scripts forem executados, aguarde o evento DOMContentLoaded antes de anexar os scripts. Se isso causar um problema de desempenho porque os scripts não começam a ser transferidos com antecedência suficiente, use tags de pré-carregamento mais cedo na página.
  • defer = true não faz nada. Se você precisar desse comportamento, execute o script manualmente quando necessário.

Etapa 3: refatorar modelos HTML e código do lado do cliente

Os manipuladores de eventos inline (como onclick="…", onerror="…") e os URIs do JavaScript (<a href="javascript:…">) podem ser usados para executar scripts. Isso significa que um invasor que encontra um bug de XSS pode injetar esse tipo de HTML e executar JavaScript malicioso. Um CSP baseado em hash ou nonce proíbe o uso desse tipo de markup. Se o site usar algum desses padrões, será necessário refatorá-los em alternativas mais seguras.

Se você tiver ativado o CSP na etapa anterior, vai ser possível conferir as violações do CSP no console sempre que ele bloquear um padrão incompatível.

Relatórios de violação do CSP no console para desenvolvedores do Chrome.
Erros do console para código bloqueado.

Na maioria dos casos, a correção é simples:

Refatorar manipuladores de eventos inline

Permitido pelo CSP
<span id="things">A thing.</span>
<script nonce="${nonce}">
  document.getElementById('things').addEventListener('click', doThings);
</script>
A CSP permite manipuladores de eventos registrados usando JavaScript.
Bloqueado pela CSP
<span onclick="doThings();">A thing.</span>
O CSP bloqueia os manipuladores de eventos inline.

Refatorar URIs javascript:

Permitido pelo CSP
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo').addEventListener('click', linkClicked);
</script>
A CSP permite manipuladores de eventos registrados usando JavaScript.
Bloqueado pela CSP
<a href="javascript:linkClicked()">foo</a>
A CSP bloqueia o javascript: URIs.

Remover eval() do JavaScript

Se o aplicativo usar eval() para converter serializações de string JSON em objetos JS, reformule essas instâncias para JSON.parse(), que também é mais rápido.

Se não for possível remover todos os usos de eval(), ainda será possível definir uma CSP rígida baseada em nonce, mas será necessário usar a palavra-chave CSP 'unsafe-eval', o que torna a política um pouco menos segura.

Confira estes e outros exemplos de refatoração neste codelab de CSP rigoroso:

Etapa 4 (opcional): adicionar substitutos para oferecer suporte a versões antigas do navegador

Compatibilidade com navegadores

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

Origem

Se você precisar de suporte para versões mais antigas do navegador:

  • O uso de strict-dynamic exige a adição de https: como fallback para versões anteriores do Safari. Ao fazer isso:
    • Todos os navegadores compatíveis com strict-dynamic ignoram o substituto https:, portanto, isso não reduz a força da política.
    • Em navegadores antigos, os scripts de origem externa só podem ser carregados se virem de uma origem HTTPS. Isso é menos seguro do que uma CSP rígida, mas ainda evita algumas causas comuns de XSS, como injeções de URIs javascript:.
  • Para garantir a compatibilidade com versões de navegador muito antigas (4 anos ou mais), adicione unsafe-inline como alternativa. Todos os navegadores recentes ignoram unsafe-inline se um valor de uso único ou hash do CSP estiver presente.
Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';

Etapa 5: implantar o CSP

Depois de confirmar que o CSP não bloqueia scripts legítimos no ambiente de desenvolvimento local, você pode implantar o CSP no ambiente de preparo e, em seguida, no ambiente de produção:

  1. (Opcional) Implante o CSP no modo somente para relatórios usando o cabeçalho Content-Security-Policy-Report-Only. O modo somente relatório é útil para testar uma mudança potencialmente crítica, como um novo CSP em produção, antes de começar a aplicar as restrições do CSP. No modo somente relatório, a CSP não afeta o comportamento do app, mas o navegador ainda gera erros de console e relatórios de violação quando encontra padrões incompatíveis com a CSP. Assim, você pode conferir o que teria sido quebrado para os usuários finais. Para mais informações, consulte a API Reporting.
  2. Quando você tiver certeza de que o CSP não vai quebrar o site para os usuários finais, implante o CSP usando o cabeçalho de resposta Content-Security-Policy. Recomendamos configurar o CSP usando um cabeçalho HTTP no servidor porque ele é mais seguro do que uma tag <meta>. Depois de concluir essa etapa, o CSP vai começar a proteger seu app contra XSS.

Limitações

Uma CSP rígida geralmente oferece uma camada extra de segurança que ajuda a mitigar XSS. Na maioria dos casos, a CSP reduz significativamente a superfície de ataque, rejeitando padrões perigosos, como URIs javascript:. No entanto, com base no tipo de CSP que você está usando (nonces, hashes, com ou sem 'strict-dynamic'), há casos em que a CSP não protege o app:

  • Se você definir um script, mas houver uma injeção diretamente no corpo ou no parâmetro src desse elemento <script>.
  • Se houver injeções nos locais de scripts criados dinamicamente (document.createElement('script')), incluindo em todas as funções de biblioteca que criam nós DOM script com base nos valores dos argumentos. Isso inclui algumas APIs comuns, como .html() do jQuery, além de .get() e .post() no jQuery < 3.0.
  • Se houver injeções de modelo em aplicativos antigos do AngularJS. Um invasor que pode injetar em um modelo do AngularJS pode usá-lo para executar JavaScript arbitrário.
  • Se a política contiver 'unsafe-eval', injeções em eval(), setTimeout() e algumas outras APIs raramente usadas.

Os desenvolvedores e engenheiros de segurança precisam prestar atenção especial a esses padrões durante as revisões de código e auditorias de segurança. Confira mais detalhes sobre esses casos em Política de segurança de conteúdo: uma confusão bem-sucedida entre proteção e mitigação.

Leitura adicional