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.