Como impedir a injeção de SQL em PHP?

SQL Injection (ou Injeção SQL) é o nome dado a uma falha no código de uma aplicação qualquer que possibilita, por meio de uma entrada qualquer, a manipulação de uma consulta SQL.

Se a entrada do usuário for inserida sem modificação em uma consulta SQL, o aplicativo se tornará vulnerável à injeção de SQL , como no exemplo a seguir:

$variavel_insegura = $_POST['user_input']; 

mysql_query("INSERT INTO `table` (`column`) VALUES ('$variavel_insegura')");

Isso ocorre porque o usuário pode inserir algo como value'); DROP TABLE table;--, e a consulta se torna:

INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')

O que pode ser feito para impedir que isso aconteça?

Use instruções preparadas e consultas parametrizadas. Estas são instruções SQL que são enviadas e analisadas pelo servidor de banco de dados separadamente de quaisquer parâmetros. Dessa forma, é impossível para um invasor injetar SQL malicioso.

Você basicamente tem duas opções para conseguir isso:

1 – Usando o PDO (para qualquer driver de banco de dados suportado):

$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');

$stmt->execute(array('name' => $name));

foreach ($stmt as $row) {
    // Faça algo com $row
}

2 – Usando o MySQLi (para MySQL):

$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'

$stmt->execute();

$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
    // Faça algo com $row
}

Se você estiver se conectando a um banco de dados que não seja o MySQL, existe uma segunda opção específica do driver à qual você pode se referir (por exemplo, pg_prepare()pg_execute() no PostgreSQL). O DOP é a opção universal.

Configurando corretamente a conexão

Observe que ao usar PDO para acessar um banco de dados MySQL, as instruções reais preparadas não são usadas por padrão . Para corrigir isso, você deve desativar a emulação de instruções preparadas. Um exemplo de criação de uma conexão usando o PDO é:

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'password');

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

No exemplo acima, o modo de erro não é estritamente necessário, mas é recomendável adicioná-lo . Dessa forma, o script não irá parar com um Fatal Error quando algo der errado. E isso dá ao desenvolvedor a chance de catch qualquer erro que seja throw como PDOException.

O que é obrigatório , no entanto, é a primeira setAttribute() linha, que informa ao PDO para desativar as instruções preparadas emuladas e usar as declarações reais preparadas. Isso garante que a instrução e os valores não sejam analisados ​​pelo PHP antes de enviá-la ao servidor MySQL (dando a um possível invasor nenhuma chance de injetar SQL malicioso).

Embora você possa definir as charset opções do construtor, é importante observar que as versões ‘mais antigas’ do PHP (antes da 5.3.6) ignoravam silenciosamente o parâmetro charset no DSN.

Explicação

A instrução SQL para a qual você passa prepare é analisada e compilada pelo servidor de banco de dados. Ao especificar parâmetros (um ? ou um parâmetro nomeado, como :name no exemplo acima), você informa ao mecanismo do banco de dados onde deseja filtrar. Então, quando você chama execute, a instrução preparada é combinada com os valores dos parâmetros especificados.

O importante aqui é que os valores dos parâmetros sejam combinados com a instrução compilada, não uma string SQL. A injeção de SQL funciona enganando o script para incluir sequências maliciosas quando ele cria o SQL para enviar ao banco de dados. Portanto, enviando o SQL real separadamente dos parâmetros, você limita o risco de acabar com algo que não pretendia.

Quaisquer parâmetros que você enviar ao usar uma instrução preparada serão tratados apenas como strings (embora o mecanismo do banco de dados possa fazer alguma otimização, portanto os parâmetros também podem acabar como números, é claro). No exemplo acima, se a $name variável contiver 'Sarah'; DELETE FROM employees o resultado, seria simplesmente uma pesquisa para a sequência "'Sarah'; DELETE FROM employees" e você não terminará com uma tabela vazia .

Outro benefício do uso de instruções preparadas é que, se você executar a mesma instrução várias vezes na mesma sessão, ela será analisada e compilada apenas uma vez, fornecendo alguns ganhos de velocidade.

Ah, e desde que você perguntou sobre como fazer isso para uma inserção, aqui está um exemplo (usando o DOP):

$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');

$preparedStatement->execute(array('column' => $unsafeValue));

Instruções preparadas podem ser usadas para consultas dinâmicas?

Embora você ainda possa usar instruções preparadas para os parâmetros da consulta, a estrutura da consulta dinâmica em si não pode ser parametrizada e certos recursos da consulta não podem ser parametrizados.

Para esses cenários específicos, a melhor coisa a fazer é usar um filtro de lista de permissões que restrinja os valores possíveis.

// Lista de permissões de valor
// $dir só pode ser 'DESC', caso contrário, será 'ASC'
if (empty($dir) || $dir !== 'DESC') {
   $dir = 'ASC';
}

Eu recomendo usar o PDO (PHP Data Objects) para executar consultas SQL parametrizadas.

Isso não apenas protege contra a injeção de SQL, mas também acelera as consultas.