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

O autor selecionou 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 usados para carregá-lo e renderizá-lo. Como exemplo, quando você acessa https://www.digitalocean.com, seu navegador baixa o HTML e CSS diretamente de digitalocean.com. No entanto, imagens e outros ativos são baixados de assets.digitalocean.com, e 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á tudo isso. 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 que servem as Políticas de Segurança de Conteúdo (CSPs, na sigla em inglês).

Usando um cabeçalho CSP, um desenvolvedor pode permitir explicitamente que determinados recursos sejam executados enquanto 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 um CSP será mais seguro, pois garante que apenas recursos aprovados sejam permitidos para serem executados.

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

Pré-requisitos

Para concluir este tutorial, você precisará de:

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

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

Como pré-requisito, você instalou o Django e configurou um projeto de exemplo. A visualização padrão no Django é muito simples para demonstrar todas as capacidades do middleware CSP, então você irá 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. Chamaremos 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ê vai adicionar uma visão básica que irá 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 visão que criamos irá renderizar esta página HTML simples. Ela exibirá o texto Olá, Sammy! junto com uma imagem de Sammy, o Tubarão.

Salve e feche o arquivo quando terminar.

Para acessar esta visão, você precisará atualizar o 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 visão que você acabou de criar será agora visível quando você visitar / (quando a aplicação estiver em execução).

Salve e feche o arquivo.

Por fim, você precisará atualizar o INSTALLED_APPS para incluir testsite no 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 modelos 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 seu-endereço-de-servidor 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 seu-endereço-de-servidor:8000. A página deve se parecer com 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 para 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ê irá instalar o Middleware CSP da Mozilla no 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 no seu terminal:

  1. pip install django-csp

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

  1. nano testsite/testsite/settings.py

Com o django-csp instalado, agora você pode adicionar o middleware no 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 fortalecido em termos de segurança. 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 o 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 para a 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 o-ip-do-seu-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 está agora 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 tem 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á usando em seu site. Como exemplo, um site que usa Fontes da Adobe e vídeos incorporados do YouTube precisará permitir esses recursos. No entanto, se o seu site apenas exibe imagens de seu próprio domínio, você pode deixar as configurações de imagens com seus valores restritivos padrão.

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

O log de rede mostra que dois recursos estão sendo bloqueados pelo CSP: uma folha de estilos de fonts.googleapis.com e uma imagem de html.sammy-codes.com. Para permitir esses recursos no cabeçalho do 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. Portanto, para permitir uma imagem de html.sammy-codes.com, você 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 apenas permitir imagens de 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 estilos para aplicá-las (de https://fonts.googleapis.com). Para permitir o carregamento das fontes, adicione o seguinte à sua CSP:

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

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

Similar a permitir imagens de html.sammy-codes.com, você também permitirá folhas de estilos de fonts.googleapis.com e fontes de fonts.gstatic.com. Para contexto, a folha de estilos 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: Similar a self, existem outras palavras-chave como unsafe-inline, unsafe-eval, ou unsafe-hashes que podem ser usadas em uma CSP. É altamente recomendado que você evite usar essas regras na sua CSP. Embora elas facilitem a implementação, podem ser usadas para contornar a CSP e torná-la inútil.

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

Agora, o Google Fonts estará autorizado a carregar estilos e fontes no seu site e html.sammy-codes.com poderá carregar imagens. No entanto, ao visitar uma página no seu servidor, você pode perceber que apenas as imagens estão sendo carregadas agora. Isso ocorre porque os estilos embutidos 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 a 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ê os fará funcionar para que possa 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 na sua 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 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ê aprova. Pense em nonce como uma senha única que aprova certas partes de um site para serem executadas por uma única sessão.

Para adicionar suporte a nonces ao seu projeto, você atualizará sua CSP no arquivo 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 em linha você está autorizado a adicionar atributos nonce. CSP_INCLUDE_NONCE_IN é tratado como uma matriz, já que várias fontes de dados suportam nonces (por exemplo, style-src).

Salve e feche o arquivo.

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

Abra o index.html para edição:

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

Adicione o seguinte snippet na <head> do HTML:

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

Este snippet imprime Olá do console! no console do navegador. No entanto, como seu projeto possui uma CSP que só permite scripts em linha se 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 ao 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 em linha. Também como esperado, a mensagem do console não foi impressa e retornou um erro. Você precisará fornecer um nonce para aprová-lo.

Podes fazer isso adicionando nonce="{{request.csp_nonce}}" a este script como um atributo. Abra o 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 o seu ficheiro quando terminar.

Se atualizares a página, o script agora será executado:

Quando olhares no Inspeccionar Elemento, vais 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. Está oculto para que quaisquer scripts com acesso ao DOM não possam acedê-lo e aplicá-lo a algum outro script. Se visualizares o código-fonte da página em vez disso, é isto que o navegador recebeu:

Repara que cada vez que atualizas a página, o valor do nonce muda. Isso ocorre porque o middleware CSP no nosso projeto gera um novo nonce para cada pedido.

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

Cada pedido que o navegador faz ao teu site terá um valor de nonce único 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.

Tu atualizaste o teu projeto para funcionar com nonce, o que pode ser aplicado a múltiplos recursos. Por exemplo, podes aplicá-lo também a estilos, atualizando CSP_INCLUDE_NONCE_IN para permitir style-src. Mas há uma abordagem mais simples para aprovar recursos em linha, e é isso que vais fazer a seguir.

Usando Hashes para Permitir Estilos Inline

Outra abordagem para permitir scripts e estilos inline é com 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 os estilos não são aplicados:

No console do navegador, você encontrará um erro indicando que um estilo inline viola a CSP. (Pode haver outros erros, mas procure pelo erro relacionado ao 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. No entanto, se você modificar esses estilos, precisará obter o novo hash e substituir o antigo por ele na CSP.

Agora você aplicará o hash adicionando-o a 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 estilos 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 embutidos agora funcionam corretamente. Neste passo, você usou duas abordagens diferentes, nonces e hashes, para permitir estilos e scripts embutidos.

Mas, há um problema importante a ser abordado. CSPs são tediosos de manter, especialmente para sites grandes. Você pode precisar de uma maneira de rastrear quando o 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 fazer log e acompanhar todas as violações produzidas pelo seu CSP.

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

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

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

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

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

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

Em seguida, na parte inferior, nomeie seu projeto (para este exemplo, vamos usar 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 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 esse valor em produção.
    traces_sample_rate=1.0,

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

Este código é fornecido pelo Sentry para que possa registrar quaisquer erros que ocorram em sua aplicação. É a configuração padrão para o Sentry e inicializa o Sentry para registrar problemas em nosso servidor. Tecnicamente, você não precisa inicializar o Sentry em seu servidor para violações de CSP, mas no caso raro de haver algum problema na renderização de 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 ir para Configurações:

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

Copie o report-uri:

Adicione-o ao seu CSP assim:

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 seu arquivo. Agora, quando a aplicação da CSP causar uma violação, o Sentry a registrará neste 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 pelo número de registros, também pode definir CSP_REPORT_PERCENTAGE em settings.py para enviar apenas uma porcentagem dos registros para o Sentry.

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

Agora, sempre que houver uma violação da CSP, você será notificado e poderá ver 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 usa nonces e hashes para permitir scripts e estilos inline. Você também a configurou para enviar violações para o Sentry. Como próximo passo, confira a documentação CSP do Django para aprender mais sobre como aplicar sua CSP.

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