Como proteger sua aplicação Django com uma Política de Segurança de Conteúdo

O autor selecionou o Girls Who Code para receber uma doação como parte do programa Write for DOnations.

Introdução

Ao visitar um site, diversos recursos são utilizados para carregá-lo e renderizá-lo. Como exemplo, ao acessar https://www.digitalocean.com, seu navegador baixa o HTML e CSS diretamente de digitalocean.com. No entanto, as imagens e outros elementos são baixados de assets.digitalocean.com, e os scripts de análise são carregados de seus respectivos domínios.

Alguns sites utilizam uma infinidade de serviços, estilos e scripts diferentes para carregar e renderizar seu conteúdo, e seu navegador executará todos eles. Um navegador não sabe se o código é malicioso, então é responsabilidade do desenvolvedor proteger os usuários. Como pode haver muitos recursos em um site, ter um recurso no navegador que permita apenas recursos aprovados é uma boa maneira de garantir que os usuários não sejam comprometidos. Para isso servem as Políticas de Segurança de Conteúdo (CSPs).

Usando um cabeçalho CSP, um desenvolvedor pode permitir explicitamente que determinados recursos sejam executados, ao mesmo tempo que impede todos os outros. Como a maioria dos sites pode ter mais de 100 recursos, e cada um deve ser aprovado para a categoria específica de recurso que é, implementar um CSP pode ser uma tarefa tediosa. No entanto, um site com CSP será mais seguro, pois garante que apenas recursos aprovados sejam permitidos.

Neste tutorial, você implementará um CSP em uma aplicação básica do Django. Você irá personalizar o CSP para permitir que determinados domínios e recursos inline sejam executados. Opcionalmente, você também pode usar o Sentry para registrar violações.

Pré-requisitos

Para completar este tutorial, você precisará de:

Passo 1 — Criando uma Visualização de Demonstração

Neste passo, você modificará como sua aplicação lida com visualizações para que possa adicionar suporte ao CSP.

Como pré-requisito, você instalou o Django e configurou um projeto de amostra. A visualização padrão no Django é muito simples para demonstrar todas as capacidades do middleware CSP, então você criará uma página HTML simples para este tutorial.

Navegue até a pasta do projeto que você criou nos pré-requisitos:

  1. cd django-apps

Enquanto estiver dentro do diretório django-apps, crie seu ambiente virtual. Vamos chamá-lo de env genérico, mas você deve usar um nome que faça sentido para você e seu projeto.

  1. virtualenv env

Agora, ative o ambiente virtual com o seguinte comando:

  1. . env/bin/activate

Dentro do ambiente virtual, crie um arquivo views.py na pasta do seu projeto usando o nano, ou seu editor de texto favorito:

  1. nano django-apps/testsite/testsite/views.py

Agora, você adicionará uma visualização básica que renderizará um modelo index.html que você criará em seguida. Adicione o seguinte ao views.py:

django-apps/testsite/testsite/views.py
from django.shortcuts import render

def index(request):
    return render(request, "index.html")

Salve e feche o arquivo quando terminar.

Crie um modelo index.html em um novo diretório templates:

mkdir django-apps/testsite/testsite/templates
nano django-apps/testsite/testsite/templates/index.html

Adicione o seguinte ao index.html:

django-apps/testsite/testsite/templates/index.html
<!DOCTYPE html>
<html>
    <head>
        <title>Hello world!</title>
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
        <link
            href="https://fonts.googleapis.com/css2?family=Yellowtail&display=swap"
            rel="stylesheet"
        />
        <style>
            h1 {
                font-family: "Yellowtail", cursive;
                margin: 0.5em 0 0 0;
                color: #0069ff;
                font-size: 4em;
                line-height: 0.6;
            }

            img {
                border-radius: 100%;
                border: 6px solid #0069ff;
            }

            .center {
                text-align: center;
                position: absolute;
                top: 50vh;
                left: 50vw;
                transform: translate(-50%, -50%);
            }
        </style>
    </head>
    <body>
        <div class="center">
            <img src="https://html.sammy-codes.com/images/small-profile.jpeg" />
            <h1>Hello, Sammy!</h1>
        </div>
    </body>
</html>

A visualização que criamos renderizará esta página HTML simples. Ele exibirá o texto Olá, Sammy! junto com uma imagem de Sammy the Shark.

Salve e feche o arquivo quando terminar.

Para acessar esta visualização, você precisará atualizar urls.py:

  1. nano django-apps/testsite/testsite/urls.py

Importe o arquivo views.py e adicione uma nova rota adicionando as linhas destacadas:

django-apps/testsite/testsite/urls.py
from django.contrib import admin
from django.urls import path
from . import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.index),
]

A nova visualização que você acabou de criar agora será visualizável quando você visitar / (quando o aplicativo estiver em execução).

Salve e feche o arquivo.

Finalmente, você precisará atualizar INSTALLED_APPS para incluir testsite em settings.py:

  1. nano django-apps/testsite/testsite/settings.py
django-apps/testsite/testsite/settings.py
# ...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'testsite',
]
# ...

Aqui, você adiciona testsite à lista de aplicativos em settings.py para que o Django possa fazer algumas suposições sobre a estrutura do seu projeto. Neste caso, ele assumirá que a pasta templates contém templates do Django que você pode usar para renderizar visualizações.

A partir do diretório raiz do projeto (testsite), inicie o servidor de desenvolvimento do Django com o seguinte comando, substituindo your-server-ip pelo endereço IP do seu próprio servidor.

  1. cd ~/django-apps/testsite
  2. python manage.py runserver your-server-ip:8000

Abra um navegador e visite your-server-ip:8000. A página deve parecer semelhante a esta:

Neste ponto, a página exibe uma imagem de perfil de Sammy, o tubarão. Abaixo da imagem está o texto Olá, Sammy! em script azul.

Para parar o servidor de desenvolvimento do Django, pressione CONTROL-C.

Neste passo, você criou uma visualização básica que atua como a página inicial do seu projeto Django. Em seguida, você adicionará suporte CSP à sua aplicação.

Passo 2 — Instalando o Middleware CSP

Neste passo, você instalará e implementará um middleware CSP para que possa adicionar cabeçalhos CSP e trabalhar com recursos CSP em suas visualizações. O middleware adiciona funcionalidades adicionais a qualquer solicitação ou resposta que o Django manipula. Neste caso, o Middleware Django-CSP adiciona suporte CSP às respostas do Django.

Primeiro, você vai instalar o Middleware CSP da Mozilla em seu projeto Django usando pip, o gerenciador de pacotes do Python. Use o seguinte comando para instalar o pacote necessário do PyPi, o Índice de Pacotes do Python. Para executar o comando, você pode parar o servidor de desenvolvimento do Django usando CONTROL-C ou abrir uma nova aba em seu terminal:

  1. pip install django-csp

Em seguida, adicione o middleware às configurações do seu projeto Django. Abra settings.py:

  1. nano testsite/testsite/settings.py

Com o django-csp instalado, agora você pode adicionar o middleware em settings.py. Isso adicionará cabeçalhos CSP às suas respostas.
Adicione a seguinte linha à matriz de configuração MIDDLEWARE:

testsite/testsite/settings.py
MIDDLEWARE = [
    'csp.middleware.CSPMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

Salve e feche o arquivo quando terminar. Seu projeto Django agora suporta CSPs. No próximo passo, você começará a adicionar cabeçalhos CSP.

Passo 3 — Implementando um Cabeçalho CSP

Agora que seu projeto suporta CSPs, está pronto para ser protegido. Para conseguir isso, você configurará o projeto para adicionar cabeçalhos CSP às suas respostas. Um cabeçalho CSP é o que diz ao navegador como se comportar quando encontra um determinado tipo de conteúdo. Portanto, se o cabeçalho disser para permitir apenas imagens de um domínio específico, então o navegador só permitirá imagens desse domínio.

Usando o nano ou seu editor de texto favorito, abra o settings.py:

  1. nano testsite/testsite/settings.py

Defina as seguintes variáveis em qualquer lugar do arquivo:

testsite/testsite/settings.py
# Política de Segurança de Conteúdo

CSP_IMG_SRC = ("'self'")

CSP_STYLE_SRC = ("'self'")

CSP_SCRIPT_SRC = ("'self'")

Essas regras são o modelo básico para sua CSP. Essas linhas indicam quais fontes são permitidas para imagens, folhas de estilo e scripts, respectivamente. No momento, todas elas contêm a string 'self', o que significa que apenas recursos do seu próprio domínio são permitidos.

Salve e feche o arquivo quando terminar.

Execute seu projeto Django com o seguinte comando:

  1. python manage.py runserver your-server-ip:8000

Ao visitar seu-ip-do-servidor:8000, você verá que o site está quebrado:

Como esperado, a imagem não aparece e o texto aparece com o estilo padrão (negrito preto). Isso significa que o cabeçalho CSP está sendo aplicado, e nossa página agora está mais segura. Como a visualização que você criou anteriormente está referenciando folhas de estilo e imagens de domínios que não são seus, o navegador os bloqueia.

Seu projeto agora possui uma CSP funcional que está instruindo o navegador a bloquear recursos que não são do seu domínio. Em seguida, você modificará a CSP para permitir recursos específicos, o que corrigirá a falta de imagem e estilo na página inicial.

Passo 4 — Modificando a CSP para Permitir Recursos Externos

Agora que você tem um CSP básico, você irá modificá-lo com base no que está utilizando em seu site. Como exemplo, um site que utiliza Fontes da Adobe e vídeos do YouTube incorporados precisará permitir esses recursos. No entanto, se o seu site apenas exibir imagens do próprio domínio, você pode deixar as configurações de imagens com seus valores padrão restritivos.

O primeiro passo é encontrar todos os recursos que você precisa aprovar. Você pode usar as ferramentas de desenvolvedor do seu navegador para isso. Abra o Monitor de Rede no Inspetor de Elementos, atualize a página e observe os recursos bloqueados:

O log de Rede mostra que dois recursos estão sendo bloqueados pelo CSP: uma folha de estilo de fonts.googleapis.com e uma imagem de html.sammy-codes.com. Para permitir esses recursos no cabeçalho CSP, você precisará modificar as variáveis em settings.py.

Para permitir recursos de domínios externos, adicione o domínio à parte do CSP que corresponde ao tipo de arquivo. Então, para permitir uma imagem de html.sammy-codes.com, você irá adicionar html.sammy-codes.com ao CSP_STYLE_SRC.

Abra settings.py e adicione o seguinte à variável CSP_STYLE_SRC:

testsite/testsite/settings.py
CSP_IMG_SRC = ("'self'", 'https://html.sammy-codes.com')

Agora, em vez de permitir apenas imagens do seu domínio, o site também permite imagens de html.sammy-codes.com.

A visualização do índice utiliza Google Fonts. O Google fornece ao seu site as fontes (de https://fonts.gstatic.com) e uma folha de estilo para aplicá-las (de https://fonts.googleapis.com). Para permitir o carregamento das fontes, adicione o seguinte ao seu CSP:

testsite/testsite/settings.py
CSP_STYLE_SRC = ("'self'", 'https://fonts.googleapis.com')

CSP_FONT_SRC = ("'self'", 'https://fonts.gstatic.com/')

Similar à permissão para imagens de html.sammy-codes.com, você também permitirá folhas de estilo de fonts.googleapis.com e fontes de fonts.gstatic.com. Para contexto, a folha de estilo carregada de fonts.googleapis.com é usada para aplicar as fontes. As próprias fontes são carregadas de fonts.gstatic.com.

Salve e feche o arquivo.

Aviso: Assim como self, existem outras palavras-chave como unsafe-inline, unsafe-eval, ou unsafe-hashes que podem ser usadas em um CSP. É altamente recomendado que você evite usar essas regras em seu CSP. Embora isso facilite a implementação, elas podem ser usadas para contornar o CSP e torná-lo inútil.

Para mais informações, consulte a documentação do produto Mozilla para “Script inline inseguro”.

Agora, o Google Fonts poderá carregar estilos e fontes em seu site e html.sammy-codes.com poderá carregar imagens. No entanto, ao visitar uma página em seu servidor, você pode notar que apenas as imagens estão sendo carregadas agora. Isso ocorre porque os estilos inline no HTML que são usados para aplicar as fontes não são permitidos. Você corrigirá isso no próximo passo.

Passo 5 — Trabalhando com Scripts e Estilos Inline

Neste ponto, você modificou o CSP para permitir recursos externos. Mas recursos inline, como estilos e scripts em sua visualização, ainda não são permitidos. Neste passo, você fará com que eles funcionem para poder aplicar estilos de fonte.

Há duas maneiras de permitir scripts e estilos inline: nonces e hashes. Se você perceber que está frequentemente modificando scripts e estilos inline, use nonces para evitar alterações frequentes em seu CSP. Se raramente atualiza scripts e estilos inline, usar hashes é uma abordagem razoável.

Usando nonce para Permitir Scripts Inline

Primeiro, você usará a abordagem de nonce. Um nonce é um token gerado aleatoriamente que é único para cada solicitação. Se duas pessoas visitarem seu site, cada uma receberá um nonce único que é incorporado nos scripts e estilos inline que você aprovar. Pense no nonce como uma senha única que aprova certas partes de um site para serem executadas em uma única sessão.

Para adicionar suporte a nonce ao seu projeto, você atualizará seu CSP no settings.py. Abra o arquivo para edição:

  1. nano testsite/testsite/settings.py

Adicione script-src em CSP_INCLUDE_NONCE_IN no arquivo settings.py.

Defina CSP_INCLUDE_NONCE_IN em qualquer lugar do arquivo e adicione 'script-src' a ele:

testsite/testsite/settings.py
# Política de Segurança de Conteúdo

CSP_INCLUDE_NONCE_IN = ['script-src']

CSP_INCLUDE_NONCE_IN indica quais scripts inline você está autorizado a adicionar atributos nonce. CSP_INCLUDE_NONCE_IN é tratado como um array, já que várias fontes de dados suportam nonces (por exemplo, style-src).

Salve e feche o arquivo.

Agora é permitido gerar nonces para scripts inline quando você adiciona o atributo nonce a eles em seu modelo de visualização. Para testar isso, você usará um trecho de código JavaScript simples.

Abra index.html para edição:

  1. nano testsite/testsite/templates/index.html

Adicione o seguinte trecho na <head> do HTML:

testsite/testsite/templates/index.html
<script>
    console.log("Hello from the console!");
</script>

Este trecho imprime Olá pelo console! no console do navegador. No entanto, como seu projeto tem uma CSP que só permite scripts inline se eles tiverem um nonce, este script não será executado e, em vez disso, produzirá um erro.

Você pode ver este erro no console do seu navegador quando atualizar a página:

A imagem é carregada porque você permitiu recursos externos no passo anterior. Como esperado, o estilo é atualmente o padrão porque você ainda não permitiu estilos inline. Também como esperado, a mensagem do console não foi impressa e retornou um erro. Você precisará dar um nonce para aprová-lo.

Você pode fazer isso adicionando nonce="{{request.csp_nonce}}" a este script como um atributo. Abra index.html para edição e adicione a parte destacada conforme mostrado aqui:

testsite/testsite/templates/index.html
<script nonce="{{request.csp_nonce}}">
    console.log("Hello from the console!");
</script>

Salve e feche seu arquivo quando terminar.

Se você atualizar a página, o script agora será executado:

Quando você olha em Inspeção de Elementos, você notará que não há valor para o atributo:

O valor não aparece por motivos de segurança. O navegador já processou o valor. Ele está oculto para que quaisquer scripts com acesso ao DOM não possam acessá-lo e aplicá-lo a algum outro script. Se você Visualizar o código-fonte da página em vez disso, é isso que o navegador recebeu:

Observe que toda vez que você atualiza a página, o valor do nonce muda. Isso ocorre porque o middleware CSP em nosso projeto gera um novo nonce para cada solicitação.

Esses valores de nonce são anexados ao cabeçalho CSP quando o navegador recebe a resposta:

Toda solicitação que o navegador fizer ao seu site terá um valor de nonce exclusivo para esse script. Como o nonce é fornecido no cabeçalho CSP, isso significa que o servidor Django aprovou esse script específico para ser executado.

Você atualizou seu projeto para funcionar com nonce, que pode ser aplicado a vários recursos. Por exemplo, você pode aplicá-lo também a estilos, atualizando CSP_INCLUDE_NONCE_IN para permitir style-src. Mas há uma abordagem mais simples para aprovar recursos inline, e é isso que você fará em seguida.

Usando Hashes para Permitir Estilos Inline

Outra abordagem para permitir scripts e estilos inline é usando hashes. Um hash é um identificador único para um recurso inline específico.

Como exemplo, este é o estilo inline em nosso modelo:

testsite/testsite/templates/index.html
<style>
    h1 {
        font-family: "Yellowtail", cursive;
        margin: 0.5em 0 0 0;
        color: #0069ff;
        font-size: 4em;
        line-height: 0.6;
    }

    img {
        border-radius: 100%;
        border: 6px solid #0069ff;
    }

    .center {
        text-align: center;
        position: absolute;
        top: 50vh;
        left: 50vw;
        transform: translate(-50%, -50%);
    }
</style>

Atualmente, porém, os estilos não estão funcionando. Quando você visualiza o site no navegador, as imagens são carregadas com sucesso, mas as fontes e estilos não são aplicados:

No console do navegador, você encontrará um erro informando que um estilo inline viola a CSP. (Pode haver outros erros, mas procure pelo erro sobre estilo inline.)

O erro é produzido porque o estilo não é aprovado pela nossa CSP. Mas, observe que o erro fornece o hash necessário para aprovar o trecho de estilo. Este hash é único para este trecho de estilo específico. Nenhum outro trecho terá o mesmo hash. Quando este hash é colocado dentro da CSP, toda vez que este estilo específico é carregado, ele será aprovado. Mas, se você modificar esses estilos, precisará obter o novo hash e substituir o antigo na CSP.

Agora você aplicará o hash adicionando-o ao CSP_STYLE_SRC em settings.py, assim:

  1. nano testsite/testsite/settings.py
testsite/testsite/settings.py
CSP_STYLE_SRC = ("'self' 'sha256-r5bInLZB0y6ZxHFpmz7cjyYrndjwCeDLDu/1KeMikHA='", 'https://fonts.googleapis.com')

Adicionar o hash sha256-... à lista CSP_STYLE_SRC permitirá que o navegador carregue a folha de estilo sem erros.

Salve e feche o arquivo.

Agora, recarregue o site no navegador, e as fontes e estilos devem carregar com sucesso:

Os estilos e scripts em linha agora funcionam corretamente. Neste passo, você utilizou duas abordagens diferentes, nonces e hashes, para permitir estilos e scripts em linha.

Mas, há um problema importante a ser abordado. As CSPs são tediosas de manter, especialmente para sites grandes. Você pode precisar de uma maneira de rastrear quando a CSP bloqueia um recurso para poder determinar se é um recurso malicioso ou simplesmente uma parte quebrada do seu site. No próximo passo, você usará o Sentry para registrar e acompanhar todas as violações produzidas pela sua CSP.

Passo 6 — Relatando Violações com o Sentry (Opcional)

Dado o quão restritas as CSPs costumam ser, é bom saber quando ela está bloqueando conteúdo — especialmente porque bloquear conteúdo provavelmente significa que alguma funcionalidade do seu site não funcionará. Ferramentas como Sentry podem avisá-lo quando a CSP está bloqueando solicitações para os usuários. Neste passo, você configurará o Sentry para registrar e relatar violações da CSP.

Como pré-requisito, você se inscreveu para uma conta com o Sentry. Agora você criará um projeto.

No canto superior esquerdo do painel do Sentry, clique na aba Projetos:

No canto superior direito, clique no botão Criar Projeto:

Você verá uma série de logotipos com um título que diz Escolha uma plataforma. Escolha Django:

Em seguida, na parte inferior, nomeie seu projeto (para este exemplo, usaremos sammys-tutorial) e clique no botão Criar Projeto:

O Sentry fornecerá um trecho de código para adicionar ao seu arquivo settings.py. Salve este trecho para adicionar em um passo posterior.

No seu terminal, instale o SDK do Sentry:

  1. pip install --upgrade sentry-sdk

Abra o arquivo settings.py assim:

  1. nano testsite/testsite/settings.py

Adicione o seguinte ao final do arquivo e certifique-se de substituir SENTRY_DSN pelo valor do painel:

testsite/testsite/settings.py
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration

sentry_sdk.init(
    dsn="SENTRY_DSN",
    integrations=[DjangoIntegration()],

    # Defina traces_sample_rate como 1.0 para capturar 100%
    # das transações para monitoramento de desempenho.
    # Recomendamos ajustar este valor em produção.
    traces_sample_rate=1.0,

    # Se você deseja associar usuários a erros (assumindo que esteja usando
    # django.contrib.auth) você pode habilitar o envio de dados PII.
    send_default_pii=True
)

Este código é fornecido pelo Sentry para que ele possa registrar quaisquer erros que ocorram em sua aplicação. É a configuração padrão do Sentry e inicializa o Sentry para registrar problemas em nosso servidor. Tecnicamente, você não precisa inicializar o Sentry no seu servidor para violações de CSP, mas no caso raro de algum problema ao renderizar nonces ou hashes, esses erros serão registrados no Sentry.

Salve e feche o arquivo.

Em seguida, volte para o painel do seu projeto e clique no ícone de engrenagem para acessar Configurações:

Vá para a aba Cabeçalhos de Segurança:

Copie o report-uri:

Adicione-o ao seu CSP da seguinte forma:

testsite/testsite/settings.py
# Política de Segurança de Conteúdo

CSP_REPORT_URI = "your-report-uri"

Certifique-se de substituir your-report-uri pelo valor que você copiou do painel.

Salve e feche o seu arquivo. Agora, quando a aplicação da CSP causar uma violação, o Sentry irá registrá-la nesta URI. Você pode testar isso removendo um domínio ou hash do seu CSP, ou removendo o nonce do script que você adicionou anteriormente. Carregue a página no navegador e você verá o erro na página de Problemas do Sentry:

Se você se sentir sobrecarregado com a quantidade de logs, também pode definir CSP_REPORT_PERCENTAGE no arquivo settings.py para enviar apenas uma porcentagem dos logs para o Sentry.

testsite/testsite/settings.py
# Política de Segurança de Conteúdo
# Enviar 10% dos logs para o Sentry
CSP_REPORT_PERCENTAGE = 0.1

Agora, sempre que houver uma violação da CSP, você será notificado e poderá visualizar o erro no Sentry.

Conclusão

Neste artigo, você protegeu sua aplicação Django com uma política de segurança de conteúdo. Você atualizou sua política para permitir recursos externos e utiliza nonces e hashes para permitir scripts e estilos inline. Você também a configurou para enviar violações ao Sentry. Como próximo passo, confira a documentação do CSP do Django para aprender mais sobre como fazer valer sua CSP.

Source:
https://www.digitalocean.com/community/tutorials/how-to-secure-your-django-application-with-a-content-security-policy