As instruções preparadas para DOP são suficientes para impedir a injeção de SQL?

660

Digamos que eu tenha um código como este:

$dbh = new PDO("blahblah");

$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

A documentação da DOP diz:

Os parâmetros para instruções preparadas não precisam ser citados; o driver lida com você.

Isso é realmente tudo o que preciso fazer para evitar injeções de SQL? é realmente tão fácil?

Você pode assumir o MySQL se isso fizer diferença. Além disso, estou realmente curioso sobre o uso de instruções preparadas contra a injeção de SQL. Nesse contexto, não me importo com o XSS ou outras possíveis vulnerabilidades.

Mark Biek
fonte
5
melhor abordagem sétimo número resposta stackoverflow.com/questions/134099/…
NullPoiиteя

Respostas:

807

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:

  1. 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, gbke sjis. Vamos selecionar gbkaqui.

    Agora, é muito importante observar o uso SET NAMESdaqui. Isso define o conjunto de caracteres NO SERVIDOR . Há outra maneira de fazer isso, mas chegaremos lá em breve.

  2. 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 , 0x27por 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: 0xbf5cseguida 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 ...

  3. $ 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 latin1a 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 $varno gbkconjunto de caracteres, veríamos:

    OR 'OR 1 = 1 / *

    Qual é exatamente o que o ataque exige.

  4. 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 PDOnã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 é utf8que 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_ESCAPESmodo SQL, que (entre outras coisas) altera a operação do mysql_real_escape_string(). Com esse modo ativado, 0x27será substituído por em 0x2727vez de 0x5c27e, 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 0xbf27seja, ainda é 0xbf27etc.) - 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_ESCAPESmodo 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).

ircmaxell
fonte
47
Esta é a melhor resposta que eu encontrei .. você pode fornecer um link para mais referência?
StaticVariable 30/09/12
1
@nicogawenda: esse foi um erro diferente. Antes da 5.0.22, mysql_real_escape_stringnão tratava adequadamente os casos em que a conexão estava definida corretamente como BIG5 / GBK. Então, na verdade, mesmo invocar o mysql_set_charset()mysql <5.0.22 seria vulnerável a esse bug! Então, não, este post é ainda aplicável a 5.0.22 (porque mysql_real_escape_string só é charset afastado para chamadas a partir mysql_set_charset(), que é o que este post está falando sobre ignorando) ...
ircmaxell
1
@progfa Se sim ou não, você sempre deve validar sua entrada no servidor antes de fazer qualquer coisa com os dados do usuário.
Tek
2
Observe que NO_BACKSLASH_ESCAPEStambém pode introduzir novas vulnerabilidades: stackoverflow.com/a/23277864/1014813
lepix
2
@slevin, o "OR 1 = 1" é um espaço reservado para o que você quiser. Sim, está procurando um valor no nome, mas imagine que a parte "OR 1 = 1" seja "UNION SELECT * FROM users". Agora você controlar a consulta e, como tal, pode abusar dela ...
ircmaxell
515

Instruções preparadas / consultas parametrizadas são geralmente suficientes para impedir a injeção de 1ª ordem nessa instrução * . Se você usar sql dinâmico desmarcado em qualquer outro lugar do seu aplicativo, ainda estará vulnerável à injeção de 2ª ordem .

A injeção de 2ª ordem significa que os dados foram percorridos no banco de dados uma vez antes de serem incluídos em uma consulta e são muito mais difíceis de executar. AFAIK, você quase nunca vê ataques reais de 2ª ordem de engenharia, pois geralmente é mais fácil para os invasores fazerem a engenharia social, mas às vezes há bugs de segunda ordem surgindo devido a 'caracteres benignos ou similares.

Você pode realizar um ataque de injeção de 2ª ordem quando pode fazer com que um valor seja armazenado em um banco de dados que posteriormente será usado como literal em uma consulta. Como exemplo, digamos que você insira as seguintes informações como seu novo nome de usuário ao criar uma conta em um site (assumindo o MySQL DB para esta pergunta):

' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '

Se não houver outras restrições no nome de usuário, uma instrução preparada ainda garantirá que a consulta incorporada acima não seja executada no momento da inserção e armazene o valor corretamente no banco de dados. No entanto, imagine que mais tarde o aplicativo recupere seu nome de usuário do banco de dados e use a concatenação de cadeias para incluir esse valor em uma nova consulta. Você pode ver a senha de outra pessoa. Como os primeiros nomes na tabela de usuários tendem a ser administradores, você também pode ter acabado de ceder o farm. (Observe também: este é mais um motivo para não armazenar senhas em texto sem formatação!)

Vemos, então, que as instruções preparadas são suficientes para uma única consulta, mas, por si só, não são suficientes para se proteger contra ataques de injeção de sql em todo o aplicativo, porque eles não possuem um mecanismo para impor todo o acesso a um banco de dados dentro de um aplicativo. código. No entanto, usado como parte de um bom design de aplicativo - que pode incluir práticas como revisão de código ou análise estática, ou uso de um ORM, camada de dados ou camada de serviço que limita o sql dinâmico - as instruções preparadas são a principal ferramenta para resolver a injeção de sql problema.Se você seguir bons princípios de design de aplicativos, de modo que o acesso a dados seja separado do restante do programa, será fácil impor ou auditar que todas as consultas usem corretamente a parametrização. Nesse caso, a injeção de sql (primeira e segunda ordem) é completamente evitada.


* Acontece que o MySql / PHP é (bem, era) apenas idiota em lidar com parâmetros quando caracteres largos estão envolvidos, e ainda há um caso raro descrito na outra resposta altamente votada aqui que pode permitir que a injeção deslize através de um parâmetro inquerir.

Joel Coehoorn
fonte
6
Isso é interessante. Eu não estava ciente da 1ª ordem vs. 2ª ordem. Você pode elaborar um pouco mais sobre como funciona a 2ª ordem?
Mark Biek
193
Se TODAS as suas consultas forem parametrizadas, você também estará protegido contra a injeção de 2ª ordem. A injeção de 1ª ordem está esquecendo que os dados do usuário não são confiáveis. A injeção de segunda ordem está esquecendo que os dados do banco de dados não são confiáveis ​​(porque vieram originalmente do usuário).
cjm 25/09/08
6
Obrigado cjm. Eu também achei este artigo útil para explicar 2º injeções ordem: codeproject.com/KB/database/SqlInjectionAttacks.aspx
Mark Biek
49
Ah sim. Mas e quanto à injeção de terceira ordem . Tem que estar ciente disso.
troelskn
81
@troelskn que deve ser onde o desenvolvedor é a fonte de dados não confiáveis
MikeMurko
45

Não, eles nem sempre são.

Depende se você permite que a entrada do usuário seja colocada na própria consulta. Por exemplo:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

estaria vulnerável a injeções de SQL e o uso de instruções preparadas neste exemplo não funcionará, porque a entrada do usuário é usada como um identificador, não como dados. A resposta certa aqui seria usar algum tipo de filtragem / validação como:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];
$allowedTables = array('users','admins','moderators');
if (!in_array($tableToUse,$allowedTables))    
 $tableToUse = 'users';

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

Nota: você não pode usar o PDO para vincular dados que ficam fora do DDL (Data Definition Language), ou seja, isso não funciona:

$stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');

A razão pela qual o exposto acima não funciona é porque DESCe ASCnão são dados . O PDO pode escapar apenas para dados . Em segundo lugar, você não pode nem colocar 'aspas. A única maneira de permitir a classificação escolhida pelo usuário é filtrar manualmente e verificar se é DESCou não ASC.

Torre
fonte
11
Estou faltando alguma coisa aqui, mas não é o objetivo das instruções preparadas evitar o tratamento do sql como uma string? Não seria algo como $ dbh-> prepare ('SELECT * FROM: tableToUse where username =: nomedeusuário'); contornar o seu problema?
Rob Forrest
4
@RobForrest sim, você está perdendo :). Os dados que você vincula funcionam apenas para DDL (Data Definition Language). Você precisa dessas citações e escapar apropriadamente. Colocar aspas para outras partes da consulta a quebra com uma alta probabilidade. Por exemplo, SELECT * FROM 'table'pode estar errado como deveria estar SELECT * FROM `table`ou sem barras invertidas. Então, algumas coisas como ORDER BY DESConde DESCvem o usuário não pode ser simplesmente escapou. Portanto, os cenários práticos são bastante ilimitados.
Torre
8
Gostaria de saber como 6 pessoas poderiam votar de um comentário propondo um uso claramente errado de uma declaração preparada. Se eles tivessem tentado uma vez, teriam descoberto imediatamente que usar o parâmetro nomeado no lugar de um nome de tabela não funcionaria.
Félix Gagnon-Grenier
Aqui está um ótimo tutorial sobre DOP, se você quiser aprender. a2znotes.blogspot.in/2014/09/introduction-to-pdo.html
RN Kushwaha
11
Você nunca deve usar uma string de consulta / corpo POST para escolher a tabela a ser usada. Se você não possui modelos, use pelo menos a switchpara derivar o nome da tabela.
ZiggyTheHamster 26/09
29

Sim, é suficiente. A maneira como os ataques do tipo injeção funcionam, é conseguir que um intérprete (O banco de dados) avalie algo, que deveria ter sido dados, como se fosse código. Isso só é possível se você misturar código e dados na mesma mídia (por exemplo, quando você constrói uma consulta como uma string).

As consultas parametrizadas funcionam enviando o código e os dados separadamente, para que nunca seja possível encontrar um buraco nisso.

Você ainda pode estar vulnerável a outros ataques do tipo injeção. Por exemplo, se você usar os dados em uma página HTML, poderá estar sujeito a ataques do tipo XSS.

Troelskn
fonte
10
"Nunca" é maneira exagero, ao ponto de ser enganosa. Se você estiver usando instruções preparadas incorretamente, não será muito melhor do que não usá-las. (Obviamente, uma "instrução preparada" que teve a entrada do usuário injetada anula o objetivo ... mas eu já vi isso acontecer. E as instruções preparadas não podem manipular identificadores (nomes de tabelas etc.) como parâmetros. para isso, alguns dos drivers DOP emulam instruções preparadas, e há espaço para eles fazerem isso incorretamente (por exemplo, analisando o SQL pela metade). Versão curta: nunca assuma que é tão fácil.
cHao 18/07/12
29

Não, isso não é suficiente (em alguns casos específicos)! Por padrão, o PDO usa instruções preparadas emuladas ao usar o MySQL como um driver de banco de dados. Você sempre deve desativar instruções preparadas emuladas ao usar o MySQL e o PDO:

$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Outra coisa que sempre deve ser feita é definir a codificação correta do banco de dados:

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

Consulte também esta pergunta relacionada: Como posso evitar a injeção de SQL no PHP?

Observe também que isso é apenas sobre o lado do banco de dados das coisas que você ainda precisa observar quando exibir os dados. Por exemplo, usando htmlspecialchars()novamente com o estilo correto de codificação e citação.

PeeHaa
fonte
14

Pessoalmente, eu sempre executaria alguma forma de saneamento nos dados, pois você nunca pode confiar na entrada do usuário; no entanto, ao usar espaços reservados / ligação de parâmetro, os dados introduzidos são enviados ao servidor separadamente para a instrução sql e depois unidos. A chave aqui é que isso vincula os dados fornecidos a um tipo e uso específicos e elimina qualquer oportunidade de alterar a lógica da instrução SQL.

JimmyJ
fonte
1

Eaven, se você estiver impedindo o front-end de injeção de sql, usando verificações de html ou js, deverá considerar que as verificações de front-end são "ignoráveis".

Você pode desativar o js ou editar um padrão com uma ferramenta de desenvolvimento front-end (incorporada atualmente ao Firefox ou Chrome).

Portanto, para evitar a injeção de SQL, seria correto higienizar o back-end da data de entrada dentro do seu controlador.

Gostaria de sugerir que você use a função PHP nativa filter_input () para limpar os valores GET e INPUT.

Se você quiser ir em frente com segurança, para consultas sensatas ao banco de dados, gostaria de sugerir que você use expressões regulares para validar o formato dos dados. preg_match () irá ajudá-lo neste caso! Mas tome cuidado! O mecanismo Regex não é tão leve. Use-o apenas se necessário, caso contrário, o desempenho do aplicativo diminuirá.

A segurança tem um custo, mas não desperdice seu desempenho!

Exemplo fácil:

se você quiser checar se um valor recebido de GET é um número, menor que 99 se (! preg_match ('/ [0-9] {1,2} /')) {...} é mais pesado de

if (isset($value) && intval($value)) <99) {...}

Portanto, a resposta final é: "Não! As declarações preparadas para DOP não impedem todo tipo de injeção de sql"; Não impede valores inesperados, apenas concatenação inesperada

snipershady
fonte
5
Você está confundindo a injeção de SQL com outra coisa que faz com que a sua resposta completamente irrelevante
Seu senso comum