A Internet pode ser um lugar assustador.
Atualmente, gafes de segurança de alto nível parecem surgir diariamente. Temos visto vírus sendo espalhados em uma velocidade incrível, enxames de computadores comprometidos exercido como armas, a corrida armamentista incessante contra spammers, e muitos, muitos relatos de roubo de identidade de sites hackeados.
Como desenvolvedores Web, temos o dever de fazer o que for necessário para combater essas forças das trevas. Todo desenvolvedor Web precisa encarar a segurança como um aspecto fundamental da programação web. Infelizmente, verifica-se que implementar a segurança é difícil - Atacantes precisam achar apenas uma vulnerabilidade, mas os defensores têm que proteger cada uma delas.
O Django tenta atenuar esta dificuldade. Ele foi projetado para automaticamente protegê-lo de muitos dos erros comuns de segurança que novos (e mesmo experientes) desenvolvedores Web cometem. Mesmo assim, é importante entender quais são esses problemas, como o Django protege você, e - mais importante - os passos que você pode tomar para tornar o código mais seguro.
Primeiro, porém, um aviso importante: Não temos a intenção de apresentar um guia definitivo para todos os exploits de segurança Web conhecidos, e por isso não vamos tentar explicar cada vulnerabilidade de forma abrangente. Em vez disso, vamos dar um breve resumo de como os problemas de segurança se aplicam ao Django.
Se você aprender apenas uma coisa desse capítulo, será isso:
Nunca -- sob quaisquer circunstância -- confie nos dados de um navegador.
Você nunca sabe quem está do outro lado da conexão HTTP. Seria um de seus usuários, mas poderia facilmente ser um cracker nefasto procurando por uma abertura.
Quaisquer dados de qualquer natureza vindo do navegador precisa ser tratado com uma saudável dose de paranóia. Isso inclue dados que sãp tanto "na banda" (ou seja, submetido pelos formulários Web) e "fora da banda" (ou seja, cabeçalhos HTTP, cookies, e outras informações requisitadas). É trivial falsificar requisições metadata que navegadores geralmente adicionam automaticamente.
Cada uma das vulnerabilidades abordadas nesse capítulo decorrem de confiar diretamente em dados vindos da rede e depois a falta da limpeza dos dados antes de usá-los. Você deveria fazer a prática geral de se questionar continuamente, "De onde os dados vieram?"
Injeção de SQL é um exploit comum na qual um atacante altera os parâmetros
da página Web (tal como dados GET
/POST
ou URLs) para inserir trechos
arbitrários de SQL que uma ingênua aplicação Web executa diratamente em seu
banco de dados. É provavelmente a mais perigosa -- e, infelizmente, a mais
comum -- vulnerabilidade lá fora.
Essa vulnerabilidade geralmente surge ao construir o SQL "na mão" pela entrada do usuário. Por exemplo, imagine escrever uma função para reunir uma lista de informações de contato de uma página de busca de contatos. Para prevenir spammers de ler cada um dos e-mails de seu sistema, nós forçamos o usário a digitar o nome do usuário de alguém antes de fornecer seu endereço de email:
def user_contacts(request): user = request.GET['username'] sql = "SELECT * FROM user_contacts WHERE username = '%s';" % username # executa o SQL aqui...
.. nota:: Nesse exemplo, e em todos os exemplos "não faça isso" similadres a seguir, nós deliberadamente deixamos de fora a maioria do código necessário para fazer as funções realmente funcionarem. Nós não queremos que o código funcione, se alguém acidentalmente levá-lo fora do contexto.
Embora a princípio isso não pareça perigoso, ele realmente é.
Primeiramente, a nossa tentativa de proteger nossa lista inteira de e-mail falhará
com uma query habilmente construída. Pense sobre o que acontece se o atacante digitar
"' OR 'a'='a"
na caixa de consulta. Nesse caso, a query que a interpolação da string
irá construir será:
SELECT * FROM user_contacts WHERE username = '' OR 'a' = 'a';
Por permitimos um SQL inseguro na string, o atancante adicionou a cláusula OR
garantindo que cara linha seja retornada.
No entanto, esse é o ataque menos assustador. Imagine o que acontece se cada
atacante enviar "'; DELETE FROM user_contacts WHERE 'a' = 'a"
. Vamos acabar
com essa query completa:
SELECT * FROM user_contacts WHERE username = ''; DELETE FROM user_contacts WHERE 'a' = 'a';
Caramba! Nossa lista de contatos inteira será deletada imediatamente.
Embora esse problema é insidioso e as vezes difícil de detectar, a solução é simples: nunca confie nos dados submitidos pelo usuário, e sempre escape ao passar para o SQL.
O banco de dados do Django API faz isso para você. Ele automaticamente escapa todos os parâmetros especiais SQL, de acordo com convenções de citação do servidor do banco de dados que você está usando (e.x., PostgreSQL ou MySQL).
Por exemplo, nessa chamada API:
foo.get_list(bar__exact="' OR 1=1")
O Django irá escapar a entrada nesse sentido, resultando em uma declaração como esta:
SELECT * FROM foos WHERE bar = '\' OR 1=1'
Completamente inofensivo.
Isso se aplica a todos os bancos de dados do Django API, com algumas excessões:
- O método
where
argumenta para oextra()
. (Olhe o Apêndice C.) Esse parâmetro aceita SQL puro por design. - Queries feitas "na mão" usando o banco de dados de baixo nível da API. (Veja Capítulo 10.)
Em cada um dos casos, é fácil manter-se protegido. Em cada caso, evite a interpolação de string para favorecer a passagem de bind parameters. Ou seja, o exemplo que iniciou esse seção deve ser escrita seguinte forma:
from django.db import connection def user_contacts(request): user = request.GET['username'] sql = "SELECT * FROM user_contacts WHERE username = %s" cursor = connection.cursor() cursor.execute(sql, [user]) # ... do something with the results
O método baixo-nível ` execute`` pega a string SQL com o espaço reservado %s
e automaticamente escapa e insere parâmetros da lista passada como segundo argumento.
Você deve sempre construir SQL personalizados dessa forma.
Infelizmente, você não pode usar parâmetros de vinculação em todos os lugares no SQL;
eles não são permitidos como identificadores (ou seja, tabelas ou nomes de colunas).
Assim, se você precisar, dizer, construir dinamicamente uma lista de tabelas de uma
variável POST
, você precisa escapar o nome no seu código. O Django fornece uma função,
django.db.connection.ops.quote_name
, que vai escapar o identificador de acordo
com o esquema de citação atual do banco de dados.
Cross-site scripting (XSS), é encontrado em aplicações Web que falham ao
escapar conteúdos submetidos corretamente pelo usuário antes de renderizar no
HTML. Isso permite ao atacante inserir HTML arbitrário na sua página Web,
geralmente na forma da tag <script>
.
Atacantes costumam usar ataques XSS para roubar cookies e informações de sessão, ou para enganar usuários ao dar informações privadas para a pessoa errada (conhecido como pishing).
Esse tipo de ataque pode tomar diferentes formas e possui geralmente infinitas permutações, então nós vamos procurar um exemplo típico. Considere essa view "Hello, Word" extremamente simples:
from django.http import HttpResponse def say_hello(request): name = request.GET.get('name', 'world') return HttpResponse('<h1>Hello, %s!</h1>' % name)
Essa view simplesmente lê o nome do parâmetro GET
e passa esse nome para o
HTML gerado. Então, se nós acessarmos http://example.com/hello/?name=Jacob
,
a página iria conter isso:
<h1>Hello, Jacob!</h1>
Mas espere -- o que acontece se acessarmos
http://example.com/hello/?name=<i>Jacob</i>
? Então nós temos isso:
<h1>Hello, <i>Jacob</i>!</h1>
É claro, um atacante não iria usar algo começando com a tag <i>
; ele
poderia incluir um conjunto de HTML para dar um hijack em sua página com conteúdo
arbitrário. Esse tipo de ataque tem sido usado para induzir usuários a inserir
dados no que parece seu site bancário, mas na verdade é uma forma de XSS-hijacked
que envia as informações de conta de volta ao atacante.
O problema fica ainda pior se você armazenar os dados no banco de dados e depois exibi-lo em seu site. Por exemplo, o MySpace já foi encontrado vulnerável a ataques XSS dessa natureza. Um usuário inseriu um JavaScript no seu perfil que automaticamente adicionou ele aos seus amigos quando você visitou o seu perfil. Dentro de alguns dias, ele teve milhões de amigos.
Agora, isso pode parecer relativamente benígno, mas tenha em mente que esse atacante conseguiu o seu código -- não o do MySpace -- executando em seu computador. Isso viola a confiança assumida que todo código do MySpace é escrito pelo MySpace.
O MySpace fo extremamente sortudo por esse código malicioso não ter deletado automaticamente as contas dos visitantes, mudado suas senhas, inundado o site com spam, ou qualquer outro tipo de cenários de pesadelos que essa vulnerabilidade desencadeia.
A solução é simples: sempre escape qualquer conteúdo que poderia ter vindo de um usuário antes de inseri-lo no HTML.
Para se proteger, o sistema de template do Django automaticamente escapa todos os valores de variáveis. Vamos ver o que acontece se reescrevemos nosso exemplo usando o sistema de template:
# views.py from django.shortcuts import render def say_hello(request): name = request.GET.get('name', 'world') return render(request, 'hello.html', {'name': name}) # hello.html <h1>Hello, {{ name }}!</h1>
Com isso no lugar, um pedido para http://example.com/hello/name=<i>Jacob</i>
irá resultar na seguinte página:
<h1>Hello, <i>Jacob</i>!</h1>
Nós cobrimos o auto-escape do Django, anteriormente no Capítulo 4, juntamente com maneiras de transformá-lo. Mas, mesmo se você estiver usando esse recurso, você deveria ainda pegar o hábito de se perguntar, sempre, "De onde os dados vêm?" Nenhuma solução automática irá previnir o seu site de ataques XSS 100% do tempo.
Cross-site request forgery (CSRF) acontece quando um Web site malicioso engana o usuário para saber o carregamento da URL de um site na qual eles já estão autenticados -- portanto, aproveitando o estado de autenticação.
O Django possui ferramentas prontas para projeter desse tipo de ataque. Tanto os os ataques como as ferramentas são cobertas detalhadamente no Capítulo 16.
Esse não é um ataque específico, mas sim uma classe geral de ataques aos dados da sessão do usuário. Ele pode tomar uma série de diferentes formas:
O ataque man-in-the-middle, onde o atacante se infiltra nos dados da sessão à medida que viaja através da rede (ou wireless).
Session forging, onde o atacante usa o ID de sessão (obtido talvez através do ataque man-in-the-middle) para fingir ser outro usuário.
Um exemplo desses dois primeiros seria um atacante em uma loja de café usando a rede wireless do café para capturar sessão de cookie. Ela pode então usar esse cookie para representar o usuário original.
Um ataque cookie-forging, onde um atacante substitui o dado supostamente somente-leitura armazenado no cookie. O Capítulo 14 explica detalhadamente como esse cookie funciona, e um dos pontos relevantes, e que é trivial para navegadores e usuários maliciosos alterarem os cookies sem o seu conhecimento.
Há uma longa história de Web sites que armazenam cookie como
IsLoggedIn=1
or evenLoggedInAsUser=jacob
. É muito simples de explorar esses tipos de cookies.Em um nível mais sutil, porém, nunca é uma boa idéia confiar em qualquer coisa armazenada nos cookies. Você nunca sabe quem está interferindo neles.
- Session fixation, onde um atacante induz o usuário em sua configuração ou
zerando a sessão do ID do usuário.
Por exemplo, o PHP permite que os identificadores de sessão sejam passados na URL (e.x.,
http://example.com/?PHPSESSID=fa90197ca25f6ab40bb1374c510d7a32
). Um atancate que induz o usuário a clicar em um link com uma sessão ID codificada fará com que o usuário pegue aquela sessão.Fixação de sessão tem sido usado em ataques phishing para induzir usuário a colocarem informações pessoais na conta que o atacante possui. Ele pode depois logar-se na conta e recuperar os dados.
Session poisoning, onde o atacante injeta dados potencialmente perigosos na sessão do usuário -- geralmente através de um formulário da Web que o usuário submete para definir os dados da sessão.
Um exemplo regular é um site que armazena uma simples preferência do usuário (como a cor de fundo da página) em um cookie. Um atacante pode induzir o usuário a clicar em um link para enviar a "cor" que, na verdade, contêm um ataque XSS. Se a cor não estiver escapada, o usuário pode novamente injetar o código malicioso no ambiente do usuário.
Há uma série de princípios gerais que podem protegê-lo desses ataques:
Nunca permita que as informações da sessão sejam contidas na URL.
A sessão do framework Django (veja Capítulo 14) simplesmente não permite que sessões sejam contidas na URL.
Não armazene dados nos cookies diretamente. Ao invés disso, armazene a sessão ID que mapeia os dados da sessão armazenando no backend.
Se você usar a sessão built-in do Django (e.x.,
request.session
), isso é feito automaticamente para você. O único cookie que a sessão do framework usa é a sessão única ID; todos os dados da sessão são armazenadas no banco de dados.Lembre-se de escapar os dados da sessão se você for exibi-lo no template. Veja a sessão anterior XSS, e lembre-se que isso se aplica a qualquer conteúdo de usuário criado assim como qualquer dado do navegador. Você deveria tratar as informações da sessão como um usuário sendo criado.
Impedir atacantes de vasculhar as sessões de ID sempre que possível.
Embora seja quase impossível detectar se alguém sequestrou uma sessão ID, o Django não tem uma proteção built-in contra o ataque de sessão brute-force. As sessões ID são armazenas como hashes (no lugar de números sequênciais), na qual previne ataques brute-force, e o usuário vai sempre pegar uma nova sessão de ID se ele tentar um usuário não existente, prevenindo a fixação de sessão.
Observe que nenhum desses princípios e ferramentas previne ataques man-in-the-middle.
Esses tipos de ataques são quase impossíveis de detectar. Se o seu site permitir
usuários registrados para ver qualquer tipo de dados sensíveis, você deveria
sempre servir o site através de HTTPS. Além disso, se você tem um site com SSL,
você definir a configuração SESSION_COOKIE_SECURE
setting para True
; isso
irá fazer com que o Django envie sessão de cookies apenas via HTTPS.
É o irmão menos conhecido do SQL Injection, e-mail header injection, que sequestra formulários Web que enviam e-mails. Um atacante pode usar essa técnica para enviar spam pelo seu servidor de email. Qualquer formulário que constrói cabeçalhos de e-mail a partir de dados de formulários Web é vulnerável a esse tipo de ataque.
Vamos olhar para o formulário de contato regular encontrado em vários sites. Normalmente ele envia mensagens para um endereço de email codificado, portanto, não aparece vulnerável à abusos de spam à primeira vista.
No entando, a maioria dessas formas também permitem que o usuário digite seu próprio assunto de e-email (junto com um endereço "De", corpo, e por vezes outros campos). O campo assunto é usado para construir o cabeçalho "assunto" da mensagem de e-mail.
Se esse cabeçalho é escapado na construção da mensagem de e-mail, o invasor pode
apresentar alguma coisa como "hello\ncc:[email protected]"
(where "\n
"
que é um caractere para uma nova linha). Isso faria com que cabeçalhos construídos
de e-mail se transformassem em:
To: [email protected] Subject: hello cc: [email protected]
Assim como o SQL injection, se nós confiarmos na linha de assunto dada pelo usuário, nós autorizamos ele a construir configurações maliciosas de cabeçalhos, e ele pode usar seu próprio formulário de contato para enviar spam.
Podemos evitar esse ataque da mesma maneira que podemos prevenir a injeção de SQL: sempre escapando ou validando conteúdos enviados pelo usuário.
As funções padrões de email do Django (no django.core.mail
) simplesmente não permitem
novas linhas nos campos usados para construir cabeçalhos (os endereços de e para,
mais o assunto). Se você tentar usar django.core.mail.send_mail
com um assunto
que possua novas linhas, o Django irá lançar uma excessão BadHeaderError
.
Se você não usar as funções padrões de email do Django para enviar e-mails, você precisa
certificar-se de que novas linhas no cabeçalho causam erros ou são retiradas. Você
pode querer examinar a classe SafeMIMEText
no django.core.mail
para ver como
o Django faz isso.
Directory traversal é um outro ataque do estilo injeção, onde um usuário malicioso troca código dos arquivos do sistema em arquivos de leitura e/ou escrita que o servidor Web não consegue ter acesso.
Um exemplo pode ser a view que lê arquivos do disco sem tomar o cuidado de tratar o nome do arquivo:
def dump_file(request): filename = request.GET["filename"] filename = os.path.join(BASE_PATH, filename) content = open(filename).read() # ...
Embora pareça que a a view restringe o acesso de arquivo para arquivos abaixo
BASE_PATH
(usando os.path.join
), se o atacante passar em um
filename
contendo ..
(que são dois períodos, um atalho para o
"o diretório pai"), ele pode acessar arquivos "acima" BASE_PATH
. É apenas
uma questão de tempo antes que ele possa descobrir o número correto de pontos
para acessar com êxito, por exemplo, ../../../../../etc/passwd
.
Tudo o que lê arquivos sem escapar é vulnerável a este problema. Views que escrevem arquivos são tão vulneráveis, mas as conseqüências são duplamente terríveis.
Outra permutação deste problema está no código que carrega dinamicamente
módulos com base na URL ou outras informações requisitadas. Um exemplo bem divulgado
veio do mundo do Ruby on Rails. Antes de meados de 2006, o Rails usava URLs como
http://example.com/person/poke/1
diretamente para carregar módulos e chamar
métodos. O resultado foi que uma URL cuidadosamente construída poderia automaticamente
carregar códigos arbitrários, incluindo um script para resetar o banco de dados!
Se o seu código precisa estar sempre lendo ou escrevendo arquivos com base na entrada o usuário, você precisa tratar o caminho requisitado com muito cuidado para garantir que o atacante não seja capaz de escapar do diretório base que você está restringindo o acesso.
.. nota:: É desnecessário dizer, que você *nunca* deve escrever códigos que possam ler a partir de qualquer área do disco!
Um bom exemplo de como fazer isso escapando encontra-se na view padrão do Django
static content-serving (no django.views.static
). Aqui está um código relevante:
import os import posixpath # ... path = posixpath.normpath(urllib.unquote(path)) newpath = '' for part in path.split('/'): if not part: # strip empty path components continue drive, part = os.path.splitdrive(part) head, part = os.path.split(part) if part in (os.curdir, os.pardir): # strip '.' and '..' in path continue newpath = os.path.join(newpath, part).replace('\\', '/')
O Django não lê arquivos (a menos que você use a função static.serve
,
mas ele é protegido com o código mostrado apenas), então essa
vulnerabilidade não afeta muito o núcleo do código.
Além disso, o uso da abstração URLconf significa que o Django nunca carrega o código que você não tenha dito explicitamente para ser carregado. Não há nenhuma maneira de criar uma URL que faça com que o Django carregue alguma coisa não mencionada no URLconf.
Durante o desenvolvimento, ser capaz de ver tracebacks e erros em seu navegador é extremamente útil. O Django possui "muitas" e informativas mensagens debug especificas para tornar o debugging mais fácil.
No entanto, se esses erros são exibidos uma vez que o site esteja no ar, eles podem revelar aspectos do seu código e configurações que podem ajudar um atacante.
Além disso, os erros e tracebacks não são úteis para os usuários finais. A filosofia do Django é que os visitantes do site nunca deveriam ver as mensagens de erro relacionadas às aplicações. Se o seu código gera uma excessão não tratada, o visitante do site deveria não ver o traceback completo -- ou qualquer dica de trechos de código ou mensagens de erro (orientado ao programador) do Python. Em vez disso, o visitante deve ver uma amigável mensagem "Essa página está indisponível".
Naturalmente, é claro, desenvolvedores precisam ver tracebacks para depurar problemas em seus códigos. Então o framework deveria esconder todas as mensagens de erro do público, mas ele deveria mostrar eles para desenvolvedores confiáveis do site.
Como nós vimos no Capítulo 12, a configuração do Django DEBUG
controla
a exibição dessas mensagens de erro. Certifique-se de configurar isso para
False
quando você estiver pronto para implantar.
Usuários implantados sob Apache e mod_python (ver também Capítulo 12) também deveriam
garantir que possuem o PythonDebug Off
em seus arquivos de conf do Apache;
isso irá suprimir os erros que ocorrem antes do Django ter tido a chance de carregar.
Esperamos que toda essa conversa sobre problemas de segurança não seja tão intimidante. É verdade que a Web pode ser um mundo selvagem, mas com um pouco de prevenção, você pode ter um Web site mais seguro.
Tenha em mente que a segurança Web é um campo em constante mudança; se você está lendo essa versão dead-tree desse livro, não deixe de conferir dados mais atualizados de segurança para qualquer nova vulnerabilidade que for descoberta. De fato, é sempre uma boa idéia gastar algum tempo por semana ou mês pesquisando e manter-se atualizado sobre o estado da segurança das aplicações Web. É um pequeno investimento a fazer, mas a proteção que você terá para o seu site e usuários é impagável.
Você chegou ao final do nosso programa regularmente agendado. A seguir os apêndices contêm material de referência que você precisar, para seu trabalho em seus projetos Django.
Desejamos-lhe uma boa sorte ao executar seu site Django, se isso for um pequeno brinquedo para você e alguns amigos, ou o próximo Google.