Pregunta ¿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.


548
2017-09-25 15:43


origen


Respuestas:


La respuesta corta es NO, PDO se prepara no lo defenderá de todos los posibles ataques de inyección SQL. Para ciertos casos de borde oscuros.

Me estoy adaptando esta respuesta para hablar de 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 tener un personaje cuyo byte final es un ASCII \ es decir 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 notar el uso de SET NAMES aquí. Esto establece el juego 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 caracter multibyte inválido; en latin1, es la cuerda ¿'. Tenga en cuenta que en latin1  y  gbk, 0x27 en sí mismo es un literal ' personaje.

    Hemos elegido esta carga porque, si llamamos addslashes() en él, insertaríamos un ASCII \ es decir 0x5c, antes de ' personaje. Entonces terminamos con 0xbf5c27, En cual gbk es una secuencia de dos caracteres: 0xbf5c seguido por 0x27. O en otras palabras, un válido personaje seguido por un sin guardar '. 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 lo hace NO hacer verdaderas declaraciones preparadas. Los emula (para MySQL). Por lo tanto, PDO construye internamente la cadena de consulta, llamando mysql_real_escape_string() (la función API de MySQL C) en cada valor de cadena enlazada.

    La llamada a la API C para 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. Nosotros le dijimos a la servidor 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 invertida, y tenemos un colgante libre ' personaje en nuestro contenido "escapado"! De hecho, si tuviéramos que mirar $var en el gbk juego de caracteres, veríamos:

    縗 'O 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);

Esta voluntad 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 silenciosamente retroceder a emular declaraciones que MySQL no puede preparar de forma nativa: aquellas que puede son listado 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() en 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, entonces una error en mysql_real_escape_string() significaba que los caracteres multibyte no válidos, como los que están en nuestra carga útil, 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 entonces este ataque aún tendría éxito. El error fue reparado 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, entonces en versiones anteriores no poder prevenir este ataque para cada comando posible!  Ahora está expuesto como un Parámetro DSN, que debe ser utilizado 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 conjunto de caracteres vulnerable. utf8mb4 es No vulnerable y aún así puede soportar cada Caracteres Unicode: por lo que puede optar por usarlos, pero solo han estado disponibles desde MySQL 5.5.3. Una alternativa es utf8, cual es también No vulnerable y puede soportar todo el Unicode Plano Bilingüe Básico.

Alternativamente, puede habilitar el NO_BACKSLASH_ESCAPES Modo SQL, que (entre otras cosas) altera la operación de mysql_real_escape_string(). Con este modo habilitado, 0x27 será reemplazado con 0x2727 más bien que 0x5c27 y así el proceso de escape no poder crear caracteres válidos en cualquiera de las codificaciones vulnerables donde no existían previamente (es decir 0xbf27 es todavía 0xbf27 etc.) - por lo que el servidor aún rechazará la cadena como inválida. Sin embargo, ver @ eggyal's answer 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 Modern Versions of MySQL (último 5.1, todo 5.5, 5.6, etc.) Y Parámetro del juego 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 / latin1 / ascii / etc)

O

  • Habilitar NO_BACKSLASH_ESCAPES Modo SQL

Estás 100% seguro

De lo contrario, eres vulnerable a pesar de que está utilizando Declaraciones preparadas 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).


676
2017-08-30 17:22



Las declaraciones preparadas / consultas parametrizadas son generalmente suficientes para prevenir Primer orden inyección en esa declaración*. Si utiliza sql dinámico sin marcar en cualquier otro lugar de su aplicación, aún es vulnerable a 2º orden inyección.

La inyección de segundo orden significa que los datos se han ciclado 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 ataques de segundo orden de ingeniería real, ya que a los atacantes generalmente les resulta más fácil ingresar en el campo de la ingeniería social, pero a veces surgen errores de segundo orden por causas benignas. ' personajes o similar.

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 son no suficiente para proteger contra los ataques de inyección sql en toda una aplicación, ya que carecen de un mecanismo para exigir que todo el acceso a una base de datos dentro de la aplicación utilice 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 limite sql dinámico - declaraciones preparadas son la herramienta principal para resolver el problema de Inyección Sql.


492
2017-09-25 15:50



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 por datos. En segundo lugar, ni siquiera puedes poner ' citas 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.


38
2018-04-21 09:00



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.


23
2017-09-25 15:55



¡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 prevenir la inyección 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. P.ej. mediante el uso htmlspecialchars() de nuevo con el estilo correcto de codificación y cotización.


22
2017-08-30 17:00



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.


8
2017-09-25 15:50



Incluso si va a evitar front-end de inyección sql, utilizando cheques html o js, ​​debería tener en cuenta que las comprobaciones frontales son "anulables".

Puede desactivar js o editar un patrón con una herramienta de desarrollo de aplicaciones para el usuario (incorporada con Firefox o Chrome hoy en día).

Por lo tanto, para evitar la inyección de SQL, sería correcto desinfectar el backend de la fecha de entrada dentro de su controlador.

Me gustaría sugerirle que use la función PHP nativa filter_input () para desinfectar los valores GET e INPUT.

Si desea continuar con la seguridad, para consultas de bases de datos sensatas, me gustaría sugerirle que use expresiones regulares para validar el formato de datos. preg_match () te ayudará en este caso! ¡Pero cuidado! El motor Regex no es tan liviano Úselo solo si es necesario, de lo contrario el rendimiento de su aplicación disminuirá.

La seguridad tiene un costo, ¡pero no desperdicie su rendimiento!

Ejemplo fácil:

si desea verificar dos veces si un valor, recibido de GET es un número, menos de 99     if (! preg_match ('/ [0-9] {1,2} /')) {...} es heavyer de

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

Por lo tanto, la respuesta final es: "¡No, las declaraciones preparadas de PDO no impiden todo tipo de inyección de SQL"; No evita valores inesperados, solo concatenación inesperada


-1
2018-03-04 20:17