php - ¿Las declaraciones preparadas por PDO son suficientes para evitar la inyección de SQL?


Digamos que tengo un código como este:

$dbh = new PDO("blahblah");

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

La documentación de PDO dice:

Los parámetros para las declaraciones preparadas no necesitan ser citados; el conductor lo maneja por ti.

¿Es eso realmente todo lo que debo hacer para evitar las inyecciones de SQL? ¿Es realmente así de fácil?

Puede asumir MySQL si hace una diferencia. Además, realmente siento curiosidad sobre el uso de declaraciones preparadas contra la inyección de SQL. En este contexto, no me preocupan las XSS u otras posibles vulnerabilidades.



Answers



Las declaraciones preparadas / consultas parametrizadas son generalmente suficientes para evitar la inyección de primer orden en esa declaración * . Si usa sql dinámico sin marcar en cualquier otro lugar de su aplicación, aún es vulnerable a la inyección de segundo orden .

La inyección de segundo orden significa que los datos se han reciclado a través de la base de datos una vez antes de ser incluidos en una consulta, y es mucho más difícil de lograr. AFAIK, casi nunca se ven verdaderos ataques de segundo orden diseñados, ya que generalmente es más fácil para los atacantes hacer ingeniería social, pero a veces surgen errores de segundo orden debido a caracteres extra benignos o similares.

Puede realizar un ataque de inyección de segundo orden cuando puede hacer que un valor se almacene en una base de datos que luego se utiliza como un literal en una consulta. Como ejemplo, digamos que ingrese la siguiente información como su nuevo nombre de usuario al crear una cuenta en un sitio web (asumiendo MySQL DB para esta pregunta):

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

Si no hay otras restricciones en el nombre de usuario, una declaración preparada aún se aseguraría de que la consulta incrustada anterior no se ejecute en el momento de la inserción y almacene el valor correctamente en la base de datos. Sin embargo, imagine que más tarde la aplicación recupera su nombre de usuario de la base de datos y utiliza la concatenación de cadenas para incluir ese valor en una nueva consulta. Es posible que vea la contraseña de otra persona. Dado que los primeros nombres en la tabla de usuarios tienden a ser administradores, es posible que también haya regalado la granja. (También tenga en cuenta: ¡esta es una razón más para no almacenar contraseñas en texto plano!)

Vemos, entonces, que las declaraciones preparadas son suficientes para una sola consulta, pero por sí mismas no son suficientes para proteger contra los ataques de inyección sql en toda una aplicación, porque carecen de un mecanismo para exigir que todo acceso a una base de datos dentro de la aplicación código seguro Sin embargo, se usa como parte del buen diseño de la aplicación, que puede incluir prácticas tales como la revisión del código o el análisis estático, o el uso de un ORM, capa de datos o capa de servicio que limita las declaraciones dinámicas preparadas en sql. problema. Si sigue los buenos principios de diseño de la aplicación, de forma que su acceso a los datos esté separado del resto de su programa, será fácil imponer o auditar que cada consulta utilice correctamente la parametrización. En este caso, la inyección sql (tanto de primer como de segundo orden) se evita por completo.

* Resulta que MySql / PHP son (está bien, fueron) simplemente tontos sobre el manejo de parámetros cuando se trata de caracteres anchos, y todavía hay un caso raro descrito en la otra respuesta altamente votada aquí que puede permitir que la inyección se deslice a través de un parámetro consulta.




La respuesta corta es NO , PDO se prepara para que no te defienda de todos los posibles ataques de inyección SQL. Para ciertos casos de borde oscuros.

Estoy adaptando esta respuesta para hablar sobre PDO ...

La respuesta larga no es tan fácil. Se basa en un ataque demostrado aquí .

El ataque

Entonces, comencemos mostrando el 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));

En ciertas circunstancias, eso devolverá más de 1 fila. Analicemos lo que está pasando aquí:

  1. Seleccionar un conjunto de caracteres

    $pdo->query('SET NAMES gbk');

    Para que funcione este ataque, necesitamos la codificación que el servidor espera en la conexión para codificar ' como en ASCII, es decir, 0x27 y para tener algún carácter cuyo byte final sea ASCII \ ie 0x5c . Como resultado, hay 5 de tales codificaciones soportadas en MySQL 5.6 por defecto: big5 , cp932 , gb2312 , gbk y sjis . Seleccionaremos gbk aquí.

    Ahora, es muy importante tener en cuenta el uso de SET NAMES aquí. Esto establece el conjunto de caracteres EN EL SERVIDOR . Hay otra forma de hacerlo, pero llegaremos pronto.

  2. La carga útil

    La carga útil que vamos a usar para esta inyección comienza con la secuencia de bytes 0xbf27 . En gbk , ese es un carácter multibyte inválido; en latin1 , es la cadena ¿' . Tenga en cuenta que en gbk y gbk , 0x27 en sí mismo es un carácter literal.

    Hemos elegido esta carga útil porque, si llamamos a addslashes() en ella, insertaríamos un ASCII \ ie 0x5c , antes del carácter ' . Entonces terminamos con 0xbf5c27 , que en gbk es una secuencia de dos caracteres: 0xbf5c seguido de 0x27 . O en otras palabras, un personaje válido seguido de un ' guardado ' . Pero no estamos usando addslashes() . Continúa con el siguiente paso ...

  3. $ stmt-> execute ()

    Lo importante es darse cuenta aquí es que PDO por defecto NO hace verdaderas declaraciones preparadas. Los emula (para MySQL). Por lo tanto, PDO construye internamente la cadena de consulta, llamando a mysql_real_escape_string() (la función API de MySQL C) en cada valor de cadena enlazada.

    La llamada de API C a mysql_real_escape_string() difiere de addslashes() en que conoce el conjunto de caracteres de conexión. Por lo tanto, puede realizar el escape correctamente para el juego de caracteres que espera el servidor. Sin embargo, hasta este punto, el cliente piensa que todavía estamos usando latin1 para la conexión, porque nunca le dijimos lo contrario. Le dijimos al servidor que estamos usando gbk , pero el cliente todavía piensa que es latin1 .

    Por lo tanto, la llamada a mysql_real_escape_string() inserta la barra diagonal inversa, ¡y tenemos un carácter ' colgado ' en nuestro contenido "escapado"! De hecho, si tuviéramos que ver $var en el gbk caracteres gbk , veríamos:

    縗' OR 1=1 /*

    Que es exactamente lo que requiere el ataque.

  4. La consulta

    Esta parte es solo una formalidad, pero aquí está la consulta procesada:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1

Felicidades, acabas de atacar con éxito un programa usando estados de cuenta preparados por PDO ...

La solución simple

Ahora, vale la pena señalar que puede evitar esto al deshabilitar las declaraciones emuladas preparadas:

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

Esto generalmente dará como resultado una declaración verdaderamente preparada (es decir, los datos enviados en un paquete separado de la consulta). Sin embargo, tenga en cuenta que PDO recurrirá silenciosamente a las declaraciones de emulación que MySQL no puede preparar de forma nativa: las que sí pueden aparecer en el manual, pero tenga cuidado de seleccionar la versión de servidor adecuada).

La corrección correcta

El problema aquí es que no llamamos a la API de C mysql_set_charset() lugar de SET NAMES . Si lo hiciéramos, estaríamos bien siempre que usemos un lanzamiento de MySQL desde 2006.

Si está utilizando una versión anterior de MySQL, un error en mysql_real_escape_string() significaba que los caracteres multibyte no válidos, como los de nuestra carga, se trataron como bytes únicos para fines de escape, incluso si el cliente había sido informado correctamente de la codificación de la conexión y así este ataque aún tendrá éxito. El error se corrigió en MySQL 4.1.20 , 5.0.22 y 5.1.11 .

Pero la peor parte es que PDO no expone la API de C para mysql_set_charset() hasta 5.3.6, por lo que en versiones anteriores no puede evitar este ataque para todos los comandos posibles. Ahora está expuesto como un parámetro DSN , que debería usarse en lugar de SET NAMES ...

La gracia salvadora

Como dijimos al principio, para que este ataque funcione, la conexión de la base de datos debe codificarse utilizando un juego de caracteres vulnerable. utf8mb4 no es vulnerable y, sin embargo, puede admitir todos los caracteres Unicode: por lo tanto, puede optar por usarlo, pero solo ha estado disponible desde MySQL 5.5.3. Una alternativa es utf8 , que tampoco es vulnerable y puede soportar todo el plano multilingüe básico de Unicode.

Alternativamente, puede habilitar el modo SQL NO_BACKSLASH_ESCAPES , que (entre otras cosas) altera el funcionamiento de mysql_real_escape_string() . Con este modo habilitado, 0x27 se reemplazará con 0x2727 lugar de 0x5c27 y, por lo tanto, el proceso de escape no puede crear caracteres válidos en ninguna de las codificaciones vulnerables donde no existían anteriormente (es decir, 0xbf27 sigue siendo 0xbf27 etc.), por lo que el servidor rechazar la cadena como inválida Sin embargo, vea la respuesta de @ eggyal para una vulnerabilidad diferente que puede surgir del uso de este modo SQL (aunque no con PDO).

Ejemplos seguros

Los siguientes ejemplos son 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 el servidor espera 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 hemos configurado correctamente el juego de caracteres para que el cliente y el servidor coincidan.

$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 hemos desactivado las declaraciones 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 hemos establecido el juego de caracteres correctamente.

$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 MySQLi hace verdaderas declaraciones preparadas todo el tiempo.

Terminando

Si tu:

  • Utilice las versiones modernas de MySQL (último 5.1, todos los 5.5, 5.6, etc.) Y el parámetro del conjunto de caracteres DSN de PDO (en PHP ≥ 5.3.6)

O

  • No use un juego de caracteres vulnerable para la codificación de conexión (solo usa utf8 / utf8 / ascii / etc)

O

  • Habilite el modo SQL NO_BACKSLASH_ESCAPES

Estás 100% seguro

De lo contrario, eres vulnerable a pesar de que estás usando Declaraciones preparadas de PDO ...

Apéndice

He estado trabajando lentamente en un parche para cambiar el valor predeterminado de no emular prepara para una versión futura de PHP. El problema con el que me estoy metiendo es que MUCHAS pruebas se rompen cuando lo hago. Un problema es que las preparaciones emuladas solo arrojarán errores de sintaxis en la ejecución, pero las preparaciones verdaderas arrojarán errores en la preparación. Entonces eso puede causar problemas (y es parte de la razón por la cual las pruebas están funcionando mal).




No, ellos no son siempre

Depende de si permite que la entrada del usuario se coloque dentro de la consulta misma. Por ejemplo:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];

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

Sería vulnerable a las inyecciones de SQL y el uso de declaraciones preparadas en este ejemplo no funcionará, porque la entrada del usuario se utiliza como un identificador, no como datos. La respuesta correcta aquí sería usar algún tipo de filtrado / validación 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: no puede usar PDO para vincular datos que están fuera del DDL (Lenguaje de definición de datos), es decir, esto no funciona:

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

La razón por la cual lo anterior no funciona es porque DESC y ASC no son datos . PDO solo puede escapar de los datos . En segundo lugar, ni siquiera puedes poner ' comillas ' a su alrededor. La única forma de permitir la clasificación elegida por el usuario es filtrar manualmente y verificar que sea DESC o ASC .




Sí, es suficiente. La forma en que funcionan los ataques tipo inyección, es de alguna manera obtener un intérprete (La base de datos) para evaluar algo, que debería haber sido datos, como si fuera un código. Esto solo es posible si combina el código y los datos en el mismo medio (por ejemplo, cuando construye una consulta como una cadena).

Las consultas parametrizadas funcionan enviando el código y los datos por separado, por lo que nunca será posible encontrar un agujero en eso.

Sin embargo, aún puedes ser vulnerable a otros ataques tipo inyección. Por ejemplo, si usa los datos en una página HTML, podría estar sujeto a ataques de tipo XSS.




¡No, esto no es suficiente (en algunos casos específicos)! Por defecto, PDO usa declaraciones preparadas emuladas cuando usa MySQL como un controlador de base de datos. Siempre debe deshabilitar las declaraciones preparadas emuladas cuando usa MySQL y PDO:

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

Otra cosa que siempre debería hacerse es establecer la codificación correcta de la base de datos:

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

También vea esta pregunta relacionada: ¿cómo puedo evitar la inyección de SQL en PHP?

También tenga en cuenta que eso solo tiene que ver con el lado de la base de datos de las cosas que aún tendría que observar usted mismo al mostrar los datos. Por ejemplo, usando htmlspecialchars() nuevamente con el estilo correcto de codificación y cotización.




Personalmente siempre ejecutaría alguna forma de desinfección de los datos, ya que nunca se puede confiar en los datos del usuario; sin embargo, cuando se usan marcadores de posición / enlace de parámetros, los datos ingresados ​​se envían al servidor por separado a la declaración sql y luego se combinan. La clave aquí es que esto vincula los datos proporcionados a un tipo específico y un uso específico y elimina cualquier oportunidad de cambiar la lógica de la declaración SQL.