A resposta curta é NÃO , a preparação do DOP não o defenderá de todos os possíveis ataques de injeção de SQL. Para certos casos obscuros.
Estou adaptando esta resposta para falar sobre o DOP ...
A resposta longa não é tão fácil. É baseado em um ataque demonstrado aqui .
O ataque
Então, vamos começar mostrando o ataque ...
$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));
Em certas circunstâncias, isso retornará mais de uma linha. Vamos dissecar o que está acontecendo aqui:
Selecionando um conjunto de caracteres
$pdo->query('SET NAMES gbk');
Para este ataque ao trabalho, precisamos da codificação que o servidor está esperando na conexão tanto para codificar '
como no ie ASCII 0x27
e ter algum personagem cujo byte final é um ASCII \
ie 0x5c
. Como se vê, há 5 tais codificações suportadas no MySQL 5.6 por padrão: big5
, cp932
, gb2312
, gbk
e sjis
. Vamos selecionar gbk
aqui.
Agora, é muito importante observar o uso SET NAMES
daqui. Isso define o conjunto de caracteres NO SERVIDOR . Há outra maneira de fazer isso, mas chegaremos lá em breve.
A carga útil
A carga útil que vamos usar para esta injeção começa com a sequência de bytes 0xbf27
. Em gbk
, esse é um caractere multibyte inválido; dentro latin1
, é a corda ¿'
. Observe que em latin1
e gbk
, 0x27
por si só, é um '
caractere literal .
Escolhemos essa carga útil, porque, se addslashes()
a utilizássemos, inseriríamos um ASCII , \
ou seja 0x5c
, antes do '
caractere. Então, terminamos com 0xbf5c27
, que gbk
é uma sequência de dois caracteres: 0xbf5c
seguida por 0x27
. Ou, em outras palavras, um caractere válido seguido por um sem escape '
. Mas não estamos usando addslashes()
. Então, para o próximo passo ...
$ stmt-> execute ()
O importante a ser percebido aqui é que o DOP por padrão NÃO faz declarações preparadas verdadeiras. Emula-os (para MySQL). Portanto, o PDO constrói internamente a string de consulta, chamando mysql_real_escape_string()
(a função API do MySQL C) em cada valor da string vinculada.
A chamada da API C mysql_real_escape_string()
é diferente por addslashes()
conhecer o conjunto de caracteres da conexão. Portanto, ele pode executar o escape corretamente para o conjunto de caracteres que o servidor está esperando. No entanto, até o momento, o cliente pensa que ainda estamos usando latin1
a conexão, porque nunca dissemos o contrário. Dissemos ao servidor que estamos usando gbk
, mas o cliente ainda pensa que é latin1
.
Portanto, a chamada para mysql_real_escape_string()
insere a barra invertida, e temos um '
caractere livre suspenso em nosso conteúdo "escapado"! Na verdade, se estivéssemos a olhar para $var
no gbk
conjunto de caracteres, veríamos:
OR 'OR 1 = 1 / *
Qual é exatamente o que o ataque exige.
A pergunta
Esta parte é apenas uma formalidade, mas aqui está a consulta renderizada:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
Parabéns, você acabou de atacar com sucesso um programa usando as declarações preparadas do DOP ...
A correção simples
Agora, é importante notar que você pode evitar isso desativando as instruções preparadas emuladas:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Isso geralmente resultará em uma verdadeira declaração preparada (isto é, os dados sendo enviados em um pacote separado da consulta). No entanto, esteja ciente de que o PDO silenciosamente recorrerá a emulações de instruções que o MySQL não pode preparar de forma nativa: aquelas que podem ser listadas no manual, mas tome cuidado para selecionar a versão apropriada do servidor).
A correção correta
O problema aqui é que não chamamos as APIs C em mysql_set_charset()
vez de SET NAMES
. Se o fizéssemos, ficaríamos bem desde que usássemos uma versão do MySQL desde 2006.
Se você estiver usando uma versão do MySQL mais cedo, em seguida, um bug no mysql_real_escape_string()
significava que caracteres de vários bytes inválidos, como os da nossa carga foram tratados como bytes únicas para efeitos escapar mesmo se o cliente tinha sido correctamente informado sobre a codificação de conexão e assim por este ataque faria ainda tem sucesso. O bug foi corrigido no MySQL 4.1.20 , 5.0.22 e 5.1.11 .
Mas a pior parte é que PDO
não expusemos a API C mysql_set_charset()
até a 5.3.6; portanto, nas versões anteriores, não é possível impedir esse ataque para todos os comandos possíveis! Agora está exposto como um parâmetro DSN , que deve ser usado em vez de SET NAMES
...
A Graça Salvadora
Como dissemos no início, para que esse ataque funcione, a conexão com o banco de dados deve ser codificada usando um conjunto de caracteres vulneráveis. nãoutf8mb4
é vulnerável e ainda pode suportar todos os caracteres Unicode: portanto, você pode optar por usá-lo - mas ele só está disponível desde o MySQL 5.5.3. Uma alternativa é utf8
que também não é vulnerável e pode suportar todo o Plano Multilíngue Básico Unicode .
Como alternativa, você pode ativar o NO_BACKSLASH_ESCAPES
modo SQL, que (entre outras coisas) altera a operação do mysql_real_escape_string()
. Com esse modo ativado, 0x27
será substituído por em 0x2727
vez de 0x5c27
e, portanto, o processo de escape não poderá criar caracteres válidos em nenhuma das codificações vulneráveis onde elas não existiam anteriormente (ou 0xbf27
seja, ainda é 0xbf27
etc.) - para que o servidor ainda rejeite a string como inválida . No entanto, consulte a resposta da @ eggyal para uma vulnerabilidade diferente que pode surgir ao usar esse modo SQL (embora não com o PDO).
Exemplos seguros
Os seguintes exemplos são seguros:
mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
Porque o servidor está esperando utf8
...
mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
Porque definimos corretamente o conjunto de caracteres para que o cliente e o servidor correspondam.
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
Porque desativamos as instruções preparadas emuladas.
$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
Porque definimos o conjunto de caracteres corretamente.
$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
Porque o MySQLi faz verdadeiras declarações preparadas o tempo todo.
Empacotando
Se vocês:
- Use as versões modernas do MySQL (final de 5.1, todas as 5.5, 5.6, etc) E o parâmetro de charset DSN do PDO (no PHP ≥ 5.3.6)
OU
- Não use um conjunto de caracteres vulneráveis para codificação de conexão (você usa apenas
utf8
/ latin1
/ ascii
/ etc)
OU
- Ativar
NO_BACKSLASH_ESCAPES
modo SQL
Você é 100% seguro.
Caso contrário, você estará vulnerável, mesmo usando declarações preparadas para DOP ...
Termo aditivo
Eu tenho trabalhado lentamente em um patch para alterar o padrão para não emular os preparativos para uma versão futura do PHP. O problema que estou enfrentando é que muitos testes são interrompidos quando faço isso. Um problema é que as preparações emuladas só lançam erros de sintaxe na execução, mas as preparações verdadeiras lançam erros na preparação. Portanto, isso pode causar problemas (e faz parte do motivo pelo qual os testes estão funcionando).