Segurança de Bancos de Dados
Hoje em dia, bancos de dados são componentes cardinais de qualquer aplicação web
permitindo que websites forneçam conteúdo dinâmico variável. Uma vez que informações muito
sensíveis e/ou secretas podem ser guardadas em um banco de dados, proteger seus
bancos de dados é essencial.
Para retirar ou guardar qualquer informação, você precisa conectar-se ao banco
de dados, enviar uma consulta (query) legítima, pegar o resultado e fechar a conexão.
Atualmente, a linguagem mais usada para consulta nessa interação é a
Structured Query Language (SQL). Veja como um atacante pode manipular uma consulta SQL.
O PHP não pode proteger seu banco de dados sozinho. As
seções a seguir tentam ser uma introdução básica em relação a como
acessar e manipular banco de dados a partir de scripts PHP.
É preciso manter em mente essa regra simples: defesa em profundidade. Em quanto mais lugares
há ações para aumentar a proteção do banco de dados, menor a probabilidade
de um atacante ter sucesso em expor ou abusar de qualquer informação
guardada. Uma boa modelagem do esquema (schema) do banco de dados e da aplicação
para lidar com os maiores medos.
Desenhando Bancos de Dados
O primeiro passo é sempre criar o banco de dados, a não ser que você queira
usar um de terceiros. Quando um banco de dados é criado, ele é
atribuído a um dono, o mesmo que executou os comandos de criação. Normalmente, só
o dono (ou um superusuário) pode fazer algo com os objetos naquele
banco de dados, e para permitir que outros usuários usem, privilégios devem
ser concedidos.
Aplicações nunca devem conectar-se ao banco de dados como seu dono ou um
superusuário, porque esses usuários podem executar qualquer consulta que
quiserem como, por exemplo, modificar o esquema (ex.: destruindo tabelas) ou
removendo seu conteúdo completamente.
Devem ser criados usuários de bancos de dados diferentes para cada aspecto
da sua aplicação com direitos bem limitados aos objetos do banco de dados.
Apenas os privilégios necessários devem ser concedidos, e deve-se evitar que o
mesmo usuário possa interagir com o banco de dados em casos de uso diferentes. Isso
significa que se invasores conseguirem acessar seu banco de dados usando credenciais da sua
aplicação, eles só podem afetar o banco tanto quanto sua aplicação poderia.
Conectando com o Banco de Dados
Deve-se estabelecer as conexões sobre SSL para criptografar as
comunicações cliente/servidor para aumentar a segurança, ou você pode usar ssh
para criptografar a conexão de rede entre clientes e o servidor de banco de dados.
Se uma dessas opções for usada, o monitoramento do seu tráfego e obtenção
de informação sobre seu banco de dados serão dificultados para um possível atacante.
Modelo de Armazenamento Criptografado
SSL/SSH protege dados transitando de um cliente para o servidor, mas
não protege os dados guardados em um banco de dados. SSL é um
protocolo on-the-wire.
Uma vez que o atacante ganhe acesso direto ao seu banco de dados (perpassando o
servidor web), os dados armazenados podem ser expostos ou usados inadequadamente, a não ser
que a informação seja protegida pelo banco em si. Criptografar os dados é
uma boa maneira de diminuir essa ameaça, mas poucos bancos de dados oferecem
esse tipo de criptografia de dados.
A maneira mais fácil de contornar esse problema é primeiro criar seu próprio
pacote de criptografia, e então usá-lo no seus scripts PHP. O PHP pode
ajudá-lo com várias extensões, tais como OpenSSL e Sodium, cobrindo uma grande variedade de algoritmos
de criptografia. O script criptografa os dados antes de inseri-los no banco de dados e
descriptografa quando os recupera. Veja as referências para outros exemplos de como
a criptografia funciona.
Hashing
No caso de dados realmente ocultos, se sua representação bruta não for necessária
(ou seja, não será exibido), o hashing deve ser levado em consideração.
O exemplo conhecido de hashing é armazenar o hash criptográfico de uma
senha em um banco de dados, em vez da própria senha.
As funções password
fornecem uma maneira conveniente para criar o hash de dados confidenciais e trabalhar com esses hashes.
password_hash é usado para criar o hash uma determinada string usando o
algoritmo mais forte atualmente disponível e password_verify
verifica se a senha fornecida corresponde ao hash armazenado no banco de dados.
Usando campo de senha hasheado
]]>
Injeção de SQL
A injeção de SQL é uma técnica onde o agressor explora falhas
no código da aplicação responsável em criar e povoar instruções SQL.
O agressor pode assim obter acesso privilegiado a partes da aplicação,
extrair todos os dados do banco de dados, alterar os dados,
e até mesmo executar comandos perigosos em nível do sistema onde o banco
de dados roda. A falha ocorre quando desenvolvedores concatenam ou
interpolam dados arbitrários em instruções SQL.
Dividindo o conjunto de resultados em páginas ... e criando super-usuários
(PostgreSQL)
No exemplo a seguir, dados de usuário são diretamente interpolados na
instrução SQL, permitindo ao agressor obter uma conta de superusuário no banco de dados.
]]>
Usuários normais clicam nos links 'próxima' e 'anterior' onde $offset
é codificado na URL. O script espera que o valor de
$offset seja um número decimal. No entanto, e se alguém tentar
quebrar a instrução SQL, utilizando a seguinte URL:
Se isso acontecesse, então o script daria de presente acesso de superusuário ao
atacante. Perceba que 0; é para fornecer uma deslocamento válido
para a consulta original e terminá-la.
É uma técnica comum forçar o avaliador de SQL ignorar o resto da consulta
escrita pelo desenvolvedor com --, que é o sinal de
comentário no SQL.
Uma maneira de ganhar senhas é desviar suas páginas de resultado de busca.
A única coisa que o atacante precisa fazer é ver se alguma variável enviada
é usada em um comando SQL que não é tratado corretamente. Esses filtros podem ser
configurados de forma a personalizar cláusulas WHERE, ORDER BY,
LIMIT e OFFSET em cláusulas SELECT
Se seu banco de dados suporta o construtor UNION,
o atacante pode tentar adicionar uma consulta inteira à consulta original para
listar senhas de uma tabela arbitrária. É altamente recomendável gravar apenas
hashs criptográficos das senhas, ao invés de gravar a senha.
Listando artigos ... e algumas senhas (qualquer banco de dados)
]]>
A parte estática da consulta pode ser combinada com outro comando
SELECT que revela todas as senhas:
Instruções UPDATE e INSERT também podem
ser abusadas em ataques.
De recuperando uma senha ... para ganhando mais privilégios (qualquer banco de dados)
]]>
Se um usuário malicioso envia o valor
' or uid like'%admin% para $uid para
mudar a senha do administrador, ou simplesmente configura $pwd para
hehehe', trusted=100, admin='yes (com um espaço
sobrando) para ganhar mais privilégios. Então, a consulta ficará retorcida:
]]>
Pode parecer que o agressor precisa saber alguma coisa
da arquitetura do banco para conduzir um ataque
efetivo, mas obter esse tipo de informação é geralmente bem simples. Por exemplo,
o código pode ser parte de um sistema open source e publicamente disponível.
Esse tipo de informação pode também pode ser obtido
em sistemas de código fechado -- mesmo no caso dele estar ofuscado ou compilado --
e mesmo através do seu próprio código, através de mensagens de erro.
Outros métodos incluem o uso de nomes típicos de tabelas e colunas. Por
exemplo, um formulário de login normalmente utiliza tabelas chamadas 'user' com
colunas chamadas 'id', 'username', e 'password'.
Atacando o sistema de um banco de dados (MSSQL Server)
Um exemplo assustador de como comandos do sistema operacional podem ser acessados
em alguns bancos de dados.
]]>
Se o atacante enviar o valor
a%' exec master..xp_cmdshell 'net user test testpass /ADD' --
para $prod, então $query terá o valor:
]]>
O MSSQL Server executa comandos SQL em um lote incluindo um comando
para adicionar um novo usuário para o banco de dados de contas locais. Se essa aplicação
estiver sendo executada como sa e o serviço MSSQLSERVER estivesse
sendo executado com privilégios suficientes, o atacante teria agora uma
conta com a qual poderia acessar essa máquina.
Alguns dos exemplos acima estão ligados a bancos específicos. Isso não
significa que um ataque similar é impossível contra outros produtos.
Seu servidor de banco de dados pode ter uma vulnerabilidade similar de outra maneira.
Um exemplo humorado dos problemas relacionados à injeção de SQL
Imagem cortesia de xkcd
Técnicas para evitar ataques
A maneira recomendada de evitar ataques de injeção de SQL é informar todos os
dados em instruções preparadas. Usar instruções parametrizadas não é o suficiente
para evitar SQL injection, mas é a maneira mais rápida e segura de fornecer dados
a instruções SQL. Todos os dados dinâmicos em WHERE,
SET, e VALUES precisam ser
substituídos por âncoras. Os dados em si serão informados durante a
execução, e serão enviados separadamente do comando SQL.
Informar dados via parâmetros deve ser utilizado apenas para dados. Outras partes dinâmicas
de uma instrução SQL precisa ser filtrada por uma lista prévia e conhecida de valores válidos.
Evitando SQL injection ao utilizar instruções preparadas PDO
prepare("SELECT * FROM products WHERE id LIKE ? ORDER BY price {$sortingOrder}");
// O valor é informado, incluindo caracteres curinga
$stmt->execute(["%{$productId}%"]);
?>
]]>
Instruções preparadas são fornecidas
no PDO,
no MySQLi,
e por outras bibliotecas de bancos de dados.
Ataques de injeção de SQL são principalmente baseados na exploração de código que não é
escrito pensando em segurança. Nunca confie em nenhum dado enviado pelo usuário,
e menos ainda em dados enviados pelo navegador, mesmo que o dado venha de um option box
ou um campo hidden, nem mesmo cookies. O primeiro exemplo mostra como
uma instrução SQL muito simples pode causar um dano desastroso.
Uma estratégia de defesa envolve várias boas práticas de codificação:
Nunca se conecte ao banco de dados utilizando um usuário administrador ou dono
dos objetos do banco. Sempre utilize usuários com privilégios mínimos.
Sempre verifique se o dado enviado tem o tipo esperado. O PHP possui
várias funções de validação de dados, de coisas simples como
as encontradas em funções de variável e
em funções de string
(por exemplo, is_numeric, ctype_digit)
a coisas mais avançadas como
suporte a
expressões regulares compatíveis com Perl.
Se a aplicação espera dados numéricos, considere verificar os dados
com ctype_digit, ou modificar os dados utilizando
settype, ou ainda reformatar o dado
com sprintf.
Se o banco de dados não suportar enviar dados por parâmetros, então
é necessário escapar todos os dados de usuário não numéricos, passando
o dado para funções específicas de escape do banco (por exemplo
mysql_real_escape_string,
sqlite_escape_string, etc).
Funções genéricas como addslashes são úteis apenas
em contextos específicos (por exemplo, no MySQL é possível modificar o
comportamento das aspas com NO_BACKSLASH_ESCAPES), então
o escape específico é necessário.
Nunca imprima nenhum dado ou erro específico do banco de dados, especialmente
dados referentes a schema. Veja também exibição de erros e funções de manipulação e log de erros.
Além disso, você ganha em relatar consultas ou dentro do script
ou no próprio banco de dados, se esse suportar. Obviamente, o relatório é incapaz
de prevenir qualquer tentativa danosa, mas pode ser útil para ajudar a
rastrear qual aplicação foi atacada. O relatório não é útil em si, mas
através da informação que ele contém. Mais detalhes geralmente é melhor que menos.