Content-Security-Policy (CSP): como melhorar a segurança do seu site

A CSP (Content Security Policy) é uma camada na defesa para seu site. Neste texto exploramos uma visão detalhada do CSP, destacando sua importância e métodos para combater vulnerabilidades como XSS e clickjacking.

Banner de divulgação do post: Entendendo como funciona o CSP - Content-Security-Policy. Computador em cima da mesa mostrando
Entendendo como funciona o CSP - Content-Security-Policy

Quando escrevemos uma página HTML, é comum incluir diversos recursos, como imagens, scripts, links, iframes, entre outros. Estes recursos podem ser fornecidos pelo próprio endereço do site, por uma fonte externa ou mesmo ser um recurso inline (<script>console.log('exemplo inline');</script>). De uma forma natural, adicionamos estes recursos de fontes confiáveis, como por exemplo um CDN:

<script
  src="https://code.jquery.com/jquery-3.7.1.min.js"
  integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" 
  crossorigin="anonymous">
</script>

Embora sempre nos esforcemos para utilizar recursos de fontes confiáveis, nosso site pode ser vulnerável, por exemplo, a ataques XSS (Cross-Site Scripting). Para ilustrar essa possibilidade, considere que muitos sites permitem a entrada de dados, como postagens de comentários, fotos, arquivos, ou qualquer outro tipo de informação. Se houver falhas no processo de validação, sanitização, etc., esses dados podem abrir brechas para ataques XSS.

Exemplo de Cross-site Scripting - XSS

Caso você não tenha familiaridade com XSS, considere o template abaixo escrito utilizando a sintaxe EJS (Embedded JavaScript templating):

💡 Sobre EJS

Em termos simples, o EJS é utilizado para criar templates reutilizáveis para páginas da web. Ele permite que você insira dados dinâmicos em seu código HTML, tornando mais fácil criar páginas que exibam informações diferentes com base em variáveis ou dados que você fornece.

Por exemplo, se você estiver construindo um site de comércio eletrônico, poderá usar o EJS para criar um modelo de página para exibir produtos. Você pode usar código JavaScript dentro do EJS para iterar sobre uma lista de produtos e gerar HTML para cada um deles.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Olá</title>
    <link href="site.css" rel="stylesheet">
  </head>
  <body>
    <img src="logotipo.png">
    <script src="jquery.js"></script>
    
    <!-- Renderiza os comentários já postados -->
    <% for (const comentario of comentarios) { %>
      <p><%= comentario %><p>
    <% } %>
    
    <!-- O resultado da execução acima será algo do tipo: -->
    <p>Comentário 1</p>
    <p>Comentário 2</p>
    <p>Comentário n</p>

    <!-- Formulário para postar um comentário -->
    <form action="/comentarios" method="post">
      <textarea name="comentario"></textarea>
      <input type="submit" value="Postar">
    </form>
  </body>
</html>

Neste exemplo, o código acima será executado no servidor e irá gerar um HTML com todos os comentários já postados. O resultado final será uma página HTML com os comentários já postados e um formulário para o usuário postar um novo comentário. Este fluxo está representado na imagem abaixo:

Diagrama de sequência mostrando a comunicação entre Cliente Servidor e Banco de Dados
Exemplo de como a requisição é processada para devolver um HTML como resposta

Agora suponha que um usuário mal intensionado poste o seguinte conteúdo no campo "comentário":

Imagem ilustrando a postagem de um conteúdo malicioso no textarea
Postagem de um conteúdo malicioso no textarea

Quando o próximo usuário requisitar a página, o trecho acima será incluído no HTML, fazendo com que o usuário seja redirecionado para uma página de login falsa:

<p>Comentário 1</p>
<p>Comentário 2</p>
<!-- Código injetado -->
<script>location.href = "https://site-falso.com/login";</script>
<p>Comentário n</p>

Exemplo de como os comentários são renderizados e entregues em um HTML como resposta da requisição

Este é um exemplo bem simples, porém didático, mas que mostra o perigo de negligenciarmos medidas de segurança. Como resolução, além da sanitização e validação, podemos utilizar outros mecanismos de proteção, como o CSP, que é o tema deste texto.

Introdução ao CSP (Content Security Policy)

De forma direta, o CSP (Content Security Policy) define uma lista de regras que o navegador deve seguir ao carregar seu site. É possível restringir alguns endereços ou mesmo comportamentos. Sem entrar ainda no detalhe, podemos definir estas regras de duas formas:

  • criar um elemento <meta> na página HTML, por exemplo:
    <meta http-equiv="Content-Security-Policy" content="...">
  • adicionar um cabeçalho na resposta da requisição HTTP:
    Content-Security-Policy: ...

Por exemplo, no problema do XSS citado mais acima, podemos restringir a execução de um script inline incluindo o valor script-src 'self' no Content-Security-Policy. Veja no exemplo a seguir:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport"
          content="width=device-width, initial-scale=1.0" />

    <!-- Definição do Content-Security-Policy -->
    <meta http-equiv="Content-Security-Policy"
          content="default-src 'self';
                   script-src 'self'" />

    <title>Teste CSP</title>
  </head>
  <body>
    <!-- Será executado pelo navegador, pois
          está no mesmo endereço da página:
          localhost:4200.
         Este script irá executar:
          console.log('sou um script'); -->
    <script src="script-local.js"></script>

    <!-- Não será executado pelo navegador -->
    <script>console.log('sou um script inline');</script>
  </body>
</html>
Navegador mostrando uma mensagem no console dizendo que o script inline não foi executado por não respeita o Content Security Policy
Content security policy impedindo que um script inline seja executado

Veja na imagem acima que o script-local.js foi executado, mas o script inline não, mostrando a mensagem:

Refused to execute a script because its hash, its nonce, or 'unsafe-inline' does not appear in the script-src directive of the Content Security Policy.

Vamos progredir no assunto criando um cenário estudo.

Criando o cenário de estudo

Para iniciar o cenário de estudo, criei os seguintes diretórios e arquivos:

<diretório de sua preferência>
 - server1 (será servido no endereço localhost:4200)
   - index.html
   - script1.js
   - img1.png
- server2 (será servido no endereço localhost:4000)
  - script2.js
  - img2.png
  - parent.html (será usado no final deste texto)

Conforme escrito acima, o conteúdo dos diretórios server1 e server2 serão servidos em endereços diferentes. A ideia é que a página index.html, que é servida por localhost:4200 busque alguns recursos em outro endereço, localhost:4000, assim conseguimos criar o cenário de estudo.

Para disponibilizar estes arquivos nestes endereços, uma forma bem rápida e simples é utilizando o pacote http-server. Este pacote cria um servidor web de forma muito simples:

# Instale o pacote em escopo global
npm http-server -g

# Entre no diretório server1 e execute:
http-server --port 4200

# Em um outro terminal, entre no diretório
# server2 e execute:
http-server --port 4000

Agora você pode acessar o conteúdo de cada diretório com seu respectivo endereço, por exemplo:

  1. http://localhost:4200/index.html
  2. http://localhost:4000/img2.png
💡
O pacote http-server é um módulo Node.js que permite iniciar um servidor HTTP simples para servir arquivos estáticos do sistema de arquivos local. Ele é útil para testar rapidamente páginas HTML, JavaScript, CSS, entre outros recursos, sem a necessidade de configurar um servidor web completo, como o Apache ou o Nginx.

Restringindo endereços com as diretivas de fetch do Content Security Policy

Com o cenário de estudo criado, vamos abordar algumas diretivas do CSP para restringir o acesso de conteúdo à endereços confiáveis. Então para iniciar, altere o conteúdo do arquivo index.html para:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0" />

    <!-- Definição do Content-Security-Policy -->
    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'self';
               img-src localhost:4000;
               script-src 'self'" />

    <title>Teste CSP</title>
  </head>
  <body>
    <p>Olá!!!</p>

    <!-- Imagens para testes-->
    <img src="img1.png" />
    <img src="http://localhost:4000/img2.png" />
    <img src="https://placehold.co/600x400/png" />

    <!-- Scripts para testes -->
    <script src="script1.js"></script>
    <script src="http://localhost:4000/script2.js"></script>
  </body>
</html>

index.html

Repare que neste arquivo há algumas imagens e alguns scripts. Também há uma tag chamada <meta> com um valor para o Content Security Policy. Seu conteúdo tem o seguinte significado:

  • img-src: Nesta configuração, especificamos que apenas imagens provenientes do endereço localhost:4000 podem ser baixadas. Isso significa que o navegador irá restringir o acesso a imagens que não estejam nesse endereço.
  • script-src: De maneira similar à configuração img-src, aqui restringimos o navegador a baixar e executar scripts apenas do endereço da própria página HTML, localhost:4200. Por esse motivo, utilizamos o valor "self".
  • default-src: Nós especificamos o img-src e script-src, mas não incluímos outras diretivas como style-src, frame-src, entre outras. Portanto, por padrão, quando não especificamos um tipo específico de recurso, o valor definido para esta diretiva será aplicado às diretivas não declaradas.

Observe o resultado abaixo:

Navegador mostrando o endereço localhost:4200. No console do DevTools há 4 mensagens dizendo que determinado recurso não pode ser carregado por não respeitar as regras do content security policy
Resultado dos valores definidos no Content Security Policy

Ao abrir a página no navegador, é possível observar que 4 recursos não foram carregados por não respeitarem as regras definidas no Content Security Policy. As mensagens que aparecem no console são:

Content-Security-Policy: The page’s settings blocked the loading of a resource (img-src) at http://localhost:4200/img1.png because it violates the following directive: “img-src http://localhost:4000” localhost:4200

Content-Security-Policy: The page’s settings blocked the loading of a resource (img-src) at https://placehold.co/600x400/png because it violates the following directive: “img-src http://localhost:4000localhost:4200

Content-Security-Policy: The page’s settings blocked the loading of a resource (img-src) at http://localhost:4200/favicon.ico because it violates the following directive: “img-src http://localhost:4000

Content-Security-Policy: The page’s settings blocked a script (script-src-elem) at http://localhost:4000/script2.js from being executed because it violates the following directive: “script-src 'self'”
💡
A mensagem de erro não é exatamente a mesma em todos os navegadores. Dependendo do navegador a mensagem é diferente, mas remete ao mesmo entendimento de erro.

Se você observar o conteúdo que o navegador não carregou, está exatamente conforme definimos no Content Security Policy.

A título de estudo, altere o conteúdo do Content Security Policy para:

<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self';
            img-src localhost:4000 'self';
            script-src 'self'" />

Com isto o navegador irá confiar em 2 endereços para carregar imagens:

  1. o localhost:4200 que é o endereço da própria página HTML, por isso adicionamos o "self"
  2. localhost:4000

Atualizando a página podemos ver que agora aparecem duas imagens:

Navegador mostrando o endereço localhost:4200. No console do DevTools há 3 mensagens dizendo que determinado recurso não pode ser carregado por não respeitar as regras do content security policy
Com o novo valor na diretiva img-src, agora duas imagens são mostradas

Deste modo podemos garantir que os recursos que a nossa página HTML está utilizando, serão de fontes confiáveis, lembrando que, este controle é feito pelo navegador. Caso o navegador seja antigo, ele irá ignorar o Content Security Policy.

Outras diretivas de fetch

Abaixo há uma lista com as diretivas mais comuns onde podemos restringir a origem de recursos:

  • default-src: Define a fonte padrão para todos os tipos de recursos.
    Exemplo de valores: 'self', 'none', 'unsafe-inline', 'unsafe-eval', 'https://example.com'
  • script-src: Define as fontes permitidas para scripts.
    Exemplo de valores: 'self', 'unsafe-inline', 'unsafe-eval', 'https://example.com'
  • style-src: Define as fontes permitidas para estilos CSS.
    Exemplo de valores: 'self', 'unsafe-inline', 'https://example.com'
  • img-src: Define as fontes permitidas para imagens.
    Exemplo de valores: 'self', 'data:', 'https://example.com'
  • font-src: Define as fontes permitidas para fontes.
    Exemplo de valores: 'self', 'data:', 'https://example.com'
  • frame-src: Define as fontes permitidas para frames e iframes.
    Exemplo de valores: 'self', 'https://example.com'
  • object-src: Define as fontes permitidas para plugins embutidos em elementos <object>.
    Exemplo de valores: 'none', 'https://example.com'
  • media-src: Define as fontes permitidas para conteúdo de áudio e vídeo.
    Exemplo de valores: 'self', 'https://media.example.com'
  • child-src: Define as fontes permitidas para criação de novos contextos de navegação (como frames e iframes).
    Exemplo de valores: 'self', 'https://child.example.com'

frame-ancestors: como evitar que seu site seja incorporado em um iframe de um outro site

Na lista acima, podemos restringir em nosso site quais endereços vamos confiar para abrir conteúdo dentro de um iframe. Por exemplo, podemos adicionar à nossa política a diretiva frame-src com o valor https://youtube.com, desta forma podemos incluir um <iframe src="https://www.youtube.com/embed/aaa"...> em nossa página.

Surge então a questão: como evitar que outro site adicione um iframe apontando para algum endereço em nosso site? Por exemplo:

<!-- Suponha que o site abaixo rode em:
     https://algum-site.com -->
<html>
  ...
  <iframe src="http://nosso-site.com/pagina1"></iframe>
</html>

Conteúdo do site hipotético https://algum-site.com

Nesse caso, podemos utilizar a diretiva frame-ancestors para especificar os endereços dos sites autorizados a adicionar um iframe que incorpore nosso site. Por exemplo, vamos especificar que somente o site hipotético site-confiavel.com.br poderá incluir um iframe cujo o src seja nosso-site.com.br.

A única diferença no uso desta diretiva, é que ela não pode ser aplicada na tag <meta>, neste caso precisamos adicioná-la no cabeçalho de resposta da nossa página.

Diagrama de sequência ilustrando o retorno do cabeçalho Content-Security-Policy na resposta da requisição
Ilustração do cabeçalho Content-Security-Policy na resposta da requisição

Para testarmos este cenário, primeiramente altere o conteúdo do arquivo parent.html que fica na pasta server2 do nosso cenário de estudo:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0" />
    <title>Parent</title>
  </head>
  <body>
    <p>Olá, sou o parent!</p>
    <iframe
      src="http://localhost:4200/index.html"
      frameborder="0"></iframe>
  </body>
</html>

parent.html

Basicamente o arquivo parent.html, que é servido no endereço localhost:4000, tem um iframe apontando para localhost:4200/index.html.

Agora, nosso objetivo é impedir que o localhost:4200/index.html seja incorporado em iframes de outros endereços. Para isto, precisamos incluir o cabeçalho Content-Security-Policy: frame-ancestors 'none' na resposta da requisição de localhost:4200/index.html.

Como no nosso cenário de estudo não temos um IIS, Nginx ou qualquer outro web server, vou utilizar um recurso bem interessante do Google Chrome, o override headers. Este recurso permite substituir ou adicionar cabeçalhos HTTP em solicitações feitas pelo navegador. Para fazer isto, siga as etapas abaixo:

  • Abra o DevTools do Google Chrome
  • Navegue até o endereço http://localhost:4000/parent.html
  • Vá até a aba Sources -> Overrides e clique em "+ Select folder for overrides"
  • Selecione um diretório de sua preferência
  • Aparecerá uma mensagem na parte superior do Chrome, dê a permissão de acesso ao diretório.
Imagem animada mostrando como habilitar a opção Overrides no DevTools do Chrome
Habilitando o override no Chrome
  • Abra a aba Network e localize o arquivo index.html
  • Clique com o botão direito e selecione a opção Override Headers
  • Adicione o cabeçalho: Content-Security-Policy: frame-ancestors 'none'
DevTools do Chrome mostrando que foi incluído um novo cabeçalho na resposta de localhost:4200/index.html
Incluindo o cabeçalho Content-Security-Policy
💡
Repare que quando usamos o override em algum recurso, aparece uma bolinha roxa no item que sofreu alteração. Isto ajuda a indicar rapidamente se há alterações de cabeçalho ou conteúdo efetuadas no DevTools pelo override.

Após realizar essa configuração, o Chrome passará a incluir o novo cabeçalho na resposta, simulando que ele foi fornecido pelo servidor, embora na realidade tenha sido configurado localmente. Em seguida, atualize a página e o iframe não será carregado, resultando na exibição da seguinte mensagem:

Refused to frame 'http://localhost:4200/' because an ancestor violates the following Content Security Policy directive: "frame-ancestors 'none'".

Deste modo conseguimos impedir que navegadores incorporem nossa página em endereços que nós não autorizarmos. Inclusive, vale mencionar que este recurso pode ajudar a prevenir ataques de clickjacking.

Considerações

O Content Security Policy (CSP) é uma ferramenta poderosa para proteger seu site contra diversas vulnerabilidades, como ataques de script entre sites (XSS) e injeção de conteúdo malicioso. Então saber utilizá-lo é muito importante.

Apesar de não ter abordado, para o texto não ficar tão longo, podemos configurar o CSP para emitir um relatório, vou deixar o link que fala sobre o assunto.

Também não listei todas as diretivas possíveis do Content Security Policy, mas este link tem a lista completa.

Neste texto, também destaquei uma das minhas funcionalidades favoritas do DevTools do Chrome: o "override headers". Além de permitir a alteração dos cabeçalhos, também é possível modificar o conteúdo da resposta. Essa capacidade é incrivelmente útil para realizar testes de forma ágil ou quando não dispomos de um ambiente de desenvolvimento preparado. Na verdade, recorri a essas funcionalidades em inúmeras ocasiões devido à sua praticidade.

Para finalizar, deixo alguns links interessantes sobre o assunto:

Links interessantes: