¿Son suficientes las declaraciones preparadas por PDO para evitar la inyección de SQL?

Resuelto Mark Biek asked hace 54 años • 7 respuestas

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 DOP dice:

No es necesario citar los parámetros de las declaraciones preparadas; el conductor lo maneja por usted.

¿Es eso realmente todo lo que necesito hacer para evitar inyecciones de SQL? ¿Es realmente tan fácil?

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

Mark Biek avatar Jan 01 '70 08:01 Mark Biek
Aceptado

La respuesta corta es , los preparados de DOP son lo suficientemente seguros si se usan correctamente.


Estoy adaptando esta respuesta para hablar de DOP...

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 determinadas 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 este ataque funcione, necesitamos que la codificación que el servidor espera en la conexión esté codificada 'como en ASCII, es decir 0x27 , y que tenga algún carácter cuyo byte final sea ASCII, es \decir 0x5c. Resulta que MySQL 5.6 admite cinco codificaciones de este tipo de forma predeterminada : big5, cp932, y . Seleccionaremos aquí.gb2312gbksjisgbk

    Ahora bien, es muy importante tener en cuenta el uso de SET NAMESaquí. Esto establece el juego de caracteres EN EL SERVIDOR . Hay otra forma de hacerlo, pero llegaremos allí pronto.

  2. La carga útil

    La carga útil que usaremos para esta inyección comienza con la secuencia de bytes 0xbf27. En gbk, ese es un carácter multibyte no válido; en latin1, es la cuerda ¿'. Tenga en cuenta que en latin1 y gbk , 0x27por sí solo es un 'carácter literal.

    Hemos elegido esta carga útil porque, si addslashes()la invocamos, insertaríamos un ASCII , \es decir 0x5c, antes del 'carácter. Entonces terminaríamos con 0xbf5c27, que gbkes una secuencia de dos caracteres: 0xbf5cseguido de 0x27. O en otras palabras, un carácter válido seguido de un archivo sin escape '. Pero no lo estamos usando addslashes(). Así que vamos al siguiente paso...

  3. $stmt->ejecutar()

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

    La llamada a la API de C se mysql_real_escape_string()diferencia addslashes()en que conoce el conjunto de caracteres de conexión. Por lo tanto, puede realizar el escape correctamente para el conjunto de caracteres que espera el servidor. Sin embargo, hasta este punto, el cliente piensa que todavía lo estamos usando latin1para 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 así latin1.

    Por lo tanto, la llamada a mysql_real_escape_string()inserta la barra invertida y tenemos un 'carácter colgante libre en nuestro contenido "escapado". De hecho, si miráramos $varel gbkconjunto de caracteres, veríamos:

    縗' OR 1=1 /*

    Que es exactamente lo que requiere el ataque.

  4. La consulta

    Esta parte es sólo una formalidad, pero aquí está la consulta representada:

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

Felicitaciones, acaba de atacar con éxito un programa utilizando declaraciones preparadas PDO...

La solución sencilla

Ahora bien, vale la pena señalar que puedes evitar esto deshabilitando las declaraciones preparadas emuladas:

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

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

La solución correcta

El problema aquí es que utilizamos SET NAMESen lugar de API de mysql_set_charset()C. De lo contrario, el ataque no tendría éxito. Pero la peor parte es que PDO no expuso la API de C hasta la mysql_set_charset()versión 5.3.6, por lo que en versiones anteriores no puede evitar este ataque para cada comando posible. Ahora está expuesto como un parámetro DSN , que debería usarse en lugar de SET NAMES ...

Esto siempre que estemos usando una versión de MySQL desde 2006. Si está usando una versión anterior de MySQL, entonces un error significó mysql_real_escape_string()que los caracteres multibyte no válidos, como los de nuestra carga útil, se trataron como bytes únicos para fines de escape, incluso si el cliente tenía sido informado correctamente de la codificación de la conexión y, por lo tanto, este ataque aún tendría éxito. El error se solucionó en MySQL 4.1.20 , 5.0.22 y 5.1.11 .

La gracia salvadora

Como decíamos al principio, para que este ataque funcione la conexión a la base de datos debe estar codificada mediante un conjunto de caracteres vulnerable. utf8mb4no es vulnerable y aun así puede admitir todos los caracteres Unicode: por lo que podría optar por usarlo en su lugar, 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 Unicode .

Alternativamente, puede habilitar el NO_BACKSLASH_ESCAPESmodo SQL, que (entre otras cosas) altera el funcionamiento de mysql_real_escape_string(). Con este modo habilitado, 0x27será reemplazado por 0x2727en lugar de 0x5c27y, 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, 0xbf27todavía está 0xbf27, etc.), por lo que el servidor seguirá rechazando la cadena como no válida. . Sin embargo, consulte la respuesta de @eggyal para conocer una vulnerabilidad diferente que puede surgir al usar 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 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 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 configurado 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 declaraciones verdaderas preparadas todo el tiempo.

Terminando

Si usted:

  • Utilice versiones modernas de MySQL (últimas 5.1, todas 5.5, 5.6, etc.) Y el parámetro de juego de caracteres DSN de PDO (en PHP ≥ 5.3.6)

O

  • No utilice un conjunto de caracteres vulnerables para la codificación de la conexión (solo utilice utf8/ latin1/ ascii/ etc.)

O

  • Habilitar NO_BACKSLASH_ESCAPESel modo SQL

Estás 100% seguro.

De lo contrario, eres vulnerable aunque estés usando declaraciones preparadas PDO...

Apéndice

He estado trabajando lentamente en un parche para cambiar el valor predeterminado para no emular preparativos para una versión futura de PHP. El problema con el que me encuentro es que MUCHAS pruebas fallan cuando hago eso. Un problema es que los preparativos emulados solo arrojarán errores de sintaxis durante la ejecución, pero los preparativos verdaderos arrojarán errores durante la preparación. Eso puede causar problemas (y es parte de la razón por la que las pruebas son aburridas).

ircmaxell avatar Aug 30 '2012 17:08 ircmaxell

Las declaraciones preparadas/consultas parametrizadas son suficientes para evitar inyecciones de SQL, pero solo cuando se usan todo el tiempo, para cada consulta en la aplicación.

Si utiliza SQL dinámico sin marcar en cualquier otro lugar de una aplicación, aún es vulnerable a la inyección de segundo orden .

La inyección de segundo orden significa que los datos han pasado por la base de datos una vez antes de incluirse en una consulta y es mucho más difícil de lograr. AFAIK, casi nunca se ven ataques de segundo orden diseñados realmente, ya que generalmente es más fácil para los atacantes realizar ingeniería social para ingresar, 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 usa como literal en una consulta. Como ejemplo, digamos que ingresa 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 garantizaría que la consulta incrustada anterior no se ejecute en el momento de la inserción y almacenaría 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 puedas ver 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. (Tenga en cuenta también: ¡esta es una razón más para no almacenar contraseñas en texto plano!)

Vemos, entonces, que si las declaraciones preparadas solo se usan para una única consulta, pero se descuidan para todas las demás consultas, esta consulta no es suficiente para proteger contra ataques de inyección SQL en toda una aplicación, porque carecen de un mecanismo para imponer todos los accesos. a una base de datos dentro de una aplicación utiliza código seguro. Sin embargo, si se usan como parte de un buen diseño de aplicaciones, que puede incluir prácticas como revisión de código o análisis estático, o el uso de un ORM, una capa de datos o una capa de servicios que limite el SQL dinámico, ** las declaraciones preparadas son la herramienta principal para resolver el problema. Problema de inyección SQL.** Si sigue buenos principios de diseño de aplicaciones, como que su acceso a los datos esté separado del resto de su programa, será fácil hacer cumplir 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 eran (hace mucho, mucho tiempo) simplemente tontos respecto al manejo de parámetros cuando se trata de caracteres anchos, y hubo un caso raro descrito en la otra respuesta altamente votada aquí que puede permitir que la inyección se escape a través de un consulta parametrizada.

Joel Coehoorn avatar Sep 25 '2008 15:09 Joel Coehoorn

No, no siempre lo son.

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 inyecciones de SQL y el uso de declaraciones preparadas en este ejemplo no funcionará, porque la entrada del usuario se utiliza como identificador, no como datos. La respuesta correcta aquí sería utilizar 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 van fuera de 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 que lo anterior no funciona es porque DESCy ASCno son datos . PDO sólo puede escapar para obtener datos . En segundo lugar, ni siquiera puedes ponerlo 'entre comillas. La única forma de permitir la clasificación elegida por el usuario es filtrar manualmente y verificar que sea DESCo ASC.

Tower avatar Apr 21 '2010 09:04 Tower

¡No, esto no es suficiente (en algunos casos específicos)! De forma predeterminada, PDO utiliza declaraciones preparadas emuladas cuando utiliza MySQL como controlador de base de datos. Siempre debes deshabilitar las declaraciones preparadas emuladas cuando uses MySQL y PDO:

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

Otra cosa que siempre se debe hacer 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');

Consulte también esta pregunta relacionada: ¿Cómo puedo evitar la inyección de SQL en PHP?

Tenga en cuenta que esto sólo le protegerá contra la inyección SQL, pero su aplicación aún podría ser vulnerable a otros tipos de ataques. Por ejemplo, puede protegerse contra XSS utilizándolo htmlspecialchars()nuevamente con la codificación y el estilo de cita correctos.

PeeHaa avatar Aug 30 '2012 17:08 PeeHaa

Sí, es suficiente. La forma en que funcionan los ataques de tipo inyección es conseguir de alguna manera que un intérprete (la base de datos) evalúe algo, que debería haber sido datos, como si fuera código. Esto sólo es posible si mezclas código y datos en el mismo medio (por ejemplo, cuando construyes una consulta como una cadena).

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

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

troelskn avatar Sep 25 '2008 15:09 troelskn