Post Image

O que é SQL Injection e como evitar no PHP?

https://images.bazaglia.com/2018/11/hacker.jpg

Evitar injeção de SQL é uma prática de segurança básica que todo programador deve saber. Passo boa parte do meu dia na internet e, ao longo dos últimos anos, percebi que a prevenção contra SQL Injections é uma prática completamente ignorada algumas vezes. Eu mesmo já acessei alguns sites vulneráveis. Não abusei da brecha, mas verifiquei que ela existia. Infelizmente, muitos sistemas ainda são vulneráveis. Esta matéria destina-se, portanto, a programadores que se dizem experientes mas ignoram práticas básicas de segurança. Me comprometo a manter as coisas claras e bem explicadas ao longo da matéria, para que mesmo programadores iniciantes entendam perfeitamente bem – e de uma vez por todas – os mitos e as verdades por traz das injeções SQL. Ao final da matéria, você será capaz de verificar se seu sistema está seguro, dentro dos padrões.

Uma ideia geral

Suponhamos que, em parte do nosso código PHP, haja a seguinte consulta no MySQL:

SELECT * FROM usuarios WHERE nome = 'andré' AND sobrenome = 'bazaglia'

Vamos considerar, ainda, que o nome e o sobrenome venham a partir de variáveis obtidas pelos métodos GET ou POST do PHP. O código referente ao "select" ficaria mais ou menos assim. O usuário preencheu os campos do formulário aonde digita seu nome e sobrenome da seguinte forma:

NOME: andre'; DROP TABLE usuarios ; --
SOBRENOME: bazaglia

Pronto. O ataque está feito. Sua tabela usuarios será zerada. Por quê? Oras, no modelo de código:

<?php

//Obtém as variáveis pelo formulário, através do método POST.
$nome = $_POST['nome'];
$sobrenome = $_POST['sobrenome'];

//Abre uma conexão com o MySQL, através do mysqli.
$mysqli = new mysqli('localhost', 'root', 'senha', 'banco');

//Query.
$sql = "SELECT * FROM usuarios WHERE nome = '$nome' AND sobrenome = '$sobrenome'";

?>

A última linha ficaria assim:

$sql = "SELECT * FROM usuarios WHERE nome = 'andre'; DROP TABLE usuarios ; --' AND sobrenome = 'bazaglia';"

Exemplifiquei usando campos de nome e sobrenome. Se seu sistema em PHP/MySQL tem uma área de login, certamente você usa, nele, um SELECT que recebe variáveis digitadas pelo usuário. Neste caso, um invasor pode simplesmente deletar sua tabela numa tentativa fraudulenta de login. Ou ainda pode se logar como outro usuário, digitando um nome de usuário válido no campo de usuário e 'OR 1=1 no campo de senha: ora, 1=1 é sempre verdadeiro. Dependendo da maneira como seu código foi escrito, o login será feito sem que uma senha válida seja necessária.

O que você nunca pode fazer

Algumas gambiarras aparentam resolver o problema. Só aparentam. São técnicas que você nunca – jamais, em hipótese alguma, combinado? – deve usar.

Função 'addslashes' do PHP.

Ao usar a função addslashes($string), o PHP adicionará uma barra invertida antes das aspas. Parece eficiente. Não é.

Em GBK, 0xbf27 não é um caractere válido, mas 0xbf5c é. Tudo o que você precisa fazer é injetar algo como 0xbf27, que será transformado algum caractere seguido de aspas. Isso acontece porque 0xbf5c é interpetado como apenas um caractere, não como dois. Pronto, você passou pela função addslashes sem que ela adicionasse uma contrabarra às suas aspas.

Função 'mysql_real_escape_string' do PHP.

A função mysql_real_escape_string é um pouco mais segura. Mas ainda não resolve seus problemas. A função mysql_real_escape_string() se difere da addslashes() por saber o charset da conexão. A proteção é feita para o charset que o servidor está esperando. Ainda assim, ainda é possível brincar de caracteres especiais. Algo como 縗' OR 1=1 /* poderia lhe causar transtornos.

Conclusão até este ponto: As estratégias apresentadas para adicionar barras invertidas às aspas, fazendo com que elas não quebrem a query, podem ser manipuladas e não são completamente seguras. Mesmo que você encontre técnicas semelhantes às duas expostas nesta matéria, fique longe delas. Precisamos de algo mais eficaz.

Como se proteger. Da maneira correta.

O PHP possui alguns drivers nativos para conexão com o banco de dados MySQL. São dois os mais indicadas: "mysqli" e "PDO". Particularmente, uso sempre o PDO. Mas é a sua opção escolher aquela que lhe agrada mais em sintaxe.

Usando PDO:

Usamos o bindParam para atribuir as variáveis ao resto da query. Se alguma irregularidade existir, o MySQL não concluirá a query. Veja o exemplo.

//Abre a conexão com o banco de dados.
$dsn = 'mysql:host=localhost;dbname=banco';
$user = 'root';
$password = 'senha';
$pdo = new PDO($dsn, $user, $password);

//Exemplo de um SELECT.
$sql= "SELECT * FROM usuarios WHERE nome = :nome AND sobrenome = :sobrenome"; 
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':nome', $nome, PDO::PARAM_STR);
$stmt->bindParam(':sobrenome', $sobrenome, PDO::PARAM_STR);
$stmt->execute();

Usando MySQLi:

É possível fazer algo similar no MySQLi. Retirei o exemplo abaixo do StackOverFlow.

$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name);

$stmt->execute();

$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
    // do something with $row
}

Nota final

Não existe, no universo da web, algo 100% seguro. Brechas de segurança são descobertas, eventualmente, até em arquiteturas consideradas absolutamente seguras e que se mantém consolidas sem que invasão sejam exploradas há muito tempo. No momento da publicação desta matéria (final de 2014), minhas sugestões são as mais indicadas para segurança. Certamente, isto não mudará tão cedo. Mas quando mudar, mais uma vez, deveremos nos atualizar às melhores práticas.