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

660

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:

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

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

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

Mark Biek
fuente
55
mejor enfoque 7º número respuesta stackoverflow.com/questions/134099/…
NullPoiиteя

Respuestas:

807

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

Estoy adaptando esta respuesta para hablar sobre 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 ciertas circunstancias, eso devolverá más de 1 fila. Analicemos lo que está sucediendo aquí:

  1. Seleccionar un conjunto de caracteres

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

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

    Ahora, es muy importante tener en cuenta el uso de SET NAMESaquí. 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 utilizar para esta inyección comienza con la secuencia de bytes 0xbf27. En gbk, ese es un carácter multibyte no válido; adentro 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 la solicitamos addslashes(), insertaríamos un ASCII , \es decir 0x5c, antes del 'carácter. Así que terminaríamos con 0xbf5c27, que gbkes una secuencia de dos caracteres: 0xbf5cseguida de 0x27. O, en otras palabras, un carácter válido seguido de un no escapado '. Pero no estamos usando addslashes(). Entonces, al siguiente paso ...

  3. $ stmt-> execute ()

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

    La llamada a la API C mysql_real_escape_string()difiere 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 el servidor espera. Sin embargo, hasta este punto, el cliente piensa que todavía estamos usando latin1la 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()insertar la barra diagonal inversa, ¡y tenemos un 'personaje que cuelga libremente en nuestro contenido "escapado"! De hecho, si tuviéramos que mirar $varen el gbkconjunto de caracteres, veríamos:

    OR '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 representada:

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

Enhorabuena, acabas de atacar con éxito un programa utilizando declaraciones preparadas de DOP ...

La solución simple

Ahora, vale la pena señalar que puede 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 que se envían en un paquete separado de la consulta). Sin embargo, tenga en cuenta que DOP silenciosamente repliegue a emular las declaraciones que MySQL no puede preparar de forma nativa: los que puede se enumeran en el manual, pero cuidado para seleccionar la versión del servidor apropiado).

La solución correcta

El problema aquí es que no llamamos a las API de C en mysql_set_charset()lugar de SET NAMES. Si lo hiciéramos, estaríamos bien siempre que usemos una versión de MySQL desde 2006.

Si está utilizando una versión de MySQL anterior, a continuación, un fallo en mysql_real_escape_string()significaban que los caracteres de varios bytes no válidos como los de nuestra carga útil fueron tratados como bytes individuales para escapar de los propósitos , incluso si el cliente había sido informado correctamente de la codificación de la conexión y por lo que este ataque sería Todavía tener éxito. El error se corrigió en MySQL 4.1.20 , 5.0.22 y 5.1.11 .

Pero la peor parte es que PDOno expuso la API de C mysql_set_charset()hasta 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 debe 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 conjunto de caracteres vulnerable. noutf8mb4 es vulnerable y, sin embargo, puede admitir todos los caracteres Unicode: por lo que puede 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 de 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 0x2727algo en 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 ( 0xbf27es decir, todavía es 0xbf27etc.), por lo que el servidor seguirá rechazando la cadena como no vá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 conjunto 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 versiones modernas de MySQL (finales 5.1, todos 5.5, 5.6, etc.) Y el parámetro de 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/ latin1/ ascii/ etc.)

O

  • Habilitar el NO_BACKSLASH_ESCAPESmodo SQL

Estás 100% seguro.

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

Apéndice

He estado trabajando lentamente en un parche para cambiar el valor predeterminado para no emular los preparativos para una futura versión de PHP. El problema con el que me encuentro es que se rompen MUCHAS pruebas cuando hago eso. 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. De modo que eso puede causar problemas (y es parte de la razón por la cual las pruebas están fallando).

ircmaxell
fuente
47
Esta es la mejor respuesta que encontré ... ¿puede proporcionar un enlace para más referencias?
StaticVariable
1
@nicogawenda: ese fue un error diferente. Antes de 5.0.22, mysql_real_escape_stringno manejaba correctamente los casos en que la conexión se configuraba correctamente en BIG5 / GBK. ¡De hecho, incluso llamar mysql_set_charset()a mysql <5.0.22 sería vulnerable a este error! Entonces, no, esta publicación todavía es aplicable a 5.0.22 (porque mysql_real_escape_string solo está alejada de las llamadas mysql_set_charset(), que es de lo que esta publicación habla sobre eludir) ...
ircmaxell
1
@progfa Ya sea que lo haga o no, siempre debe validar su entrada en el servidor antes de hacer algo con los datos del usuario.
Tek
2
Tenga en cuenta que NO_BACKSLASH_ESCAPEStambién puede introducir nuevas vulnerabilidades: stackoverflow.com/a/23277864/1014813
lepix
2
@slevin el "OR 1 = 1" es un marcador de posición para lo que quieras. Sí, está buscando un valor en el nombre, pero imagine que la parte "OR 1 = 1" era "UNION SELECT * FROM users". Ahora controlas la consulta y, como tal, puedes abusar de ella ...
ircmaxell
515

Las declaraciones preparadas / consultas parametrizadas generalmente son suficientes para evitar la inyección de primer orden en esa declaración * . Si utiliza sql dinámico no verificado 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 pasado por 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 ves ataques reales de segundo orden diseñados, ya que generalmente es más fácil para los atacantes hacer ingeniería social, pero a veces tienes errores de segundo orden debido a 'personajes benignos adicionales 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 utilizará como literal en una consulta. Como ejemplo, supongamos que ingresa la siguiente información como su nuevo nombre de usuario al crear una cuenta en un sitio web (suponiendo MySQL DB para esta pregunta):

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

Si no hay otras restricciones sobre 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. Puede 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. (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í solas son no suficiente para proteger contra ataques de inyección SQL a través de toda una aplicación, ya que carecen de un mecanismo para hacer cumplir todos los accesos a una base de datos dentro de una aplicación usos seguros código. Sin embargo, se utiliza como parte de un buen diseño de la aplicación, que puede incluir prácticas como la revisión de código o el análisis estático, o el uso de un ORM, una capa de datos o una capa de servicio que limite las declaraciones dinámicas preparadas para SQL. Es la herramienta principal para resolver la inyección SQL. problema.Si sigue buenos principios de diseño de aplicaciones, de modo que su acceso a los datos esté separado del resto de su programa, será más fácil hacer cumplir o auditar que cada consulta utilice correctamente la parametrización. En este caso, la inyección sql (de primer y segundo orden) se evita por completo.


* Resulta que MySql / PHP son (está bien, eran) tontos sobre el manejo de parámetros cuando están involucrados 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 parametrizado consulta.

Joel Coehoorn
fuente
66
Eso es interesante. No estaba al tanto del primer orden frente al segundo orden. ¿Puedes elaborar un poco más sobre cómo funciona el segundo orden?
Mark Biek
193
Si TODAS sus consultas están parametrizadas, también está protegido contra la inyección de segundo orden. La inyección de primer orden olvida que los datos del usuario no son confiables. La inyección de segundo orden olvida que los datos de la base de datos no son confiables (porque originalmente provienen del usuario).
cjm
66
Gracias cjm. También encontré este artículo útil para explicar las inyecciones de segundo orden: codeproject.com/KB/database/SqlInjectionAttacks.aspx
Mark Biek
49
Ah, sí. Pero qué pasa con la inyección de tercer orden . Hay que estar al tanto de eso.
troelskn
81
@troelskn que debe estar donde el desarrollador es la fuente de datos no confiables
MikeMurko
45

No, no siempre 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 las inyecciones de SQL y el uso de declaraciones preparadas en este ejemplo no funcionará, porque la entrada del usuario se usa 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 enlazar datos que van 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 que lo anterior no se debe a que el trabajo DESCy ASCno son datos . PDO solo puede escapar para obtener 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 si es DESCo ASC.

Torre
fuente
11
¿Me estoy perdiendo algo aquí, pero no es el objetivo de las declaraciones preparadas para evitar tratar sql como una cadena? No sería algo así como $ dbh-> prepare ('SELECT * FROM: tableToUse where username =: username'); evitar su problema?
Rob Forrest
44
@RobForrest sí te estás perdiendo :). Los datos que vincula solo funcionan para DDL (Lenguaje de definición de datos). Necesitas esas citas y un escape adecuado. Colocar comillas para otras partes de la consulta lo rompe con una alta probabilidad. Por ejemplo, SELECT * FROM 'table'puede estar equivocado como debería ser SELECT * FROM `table`o sin backsticks. Entonces algunas cosas comoORDER BY DESC dónde DESCviene el usuario no se pueden escapar simplemente. Entonces, los escenarios prácticos son bastante ilimitados.
Torre
8
Me pregunto cómo 6 personas podrían votar un comentario proponiendo un uso claramente incorrecto de una declaración preparada. Si lo hubieran probado una vez, habrían descubierto de inmediato que usar el parámetro con nombre en lugar del nombre de una tabla no funcionará.
Félix Gagnon-Grenier
Aquí hay un gran tutorial sobre PDO si quieres aprenderlo. a2znotes.blogspot.in/2014/09/introduction-to-pdo.html
RN Kushwaha
11
Nunca debe usar una cadena de consulta / cuerpo POST para elegir la tabla a usar. Si no tiene modelos, al menos use a switchpara derivar el nombre de la tabla.
ZiggyTheHamster
29

Si es suficiente. La forma en que funcionan los ataques de 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 mezcla código y 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ía posible encontrar un agujero en eso.

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

troelskn
fuente
10
"Nunca" es forma exagerando, hasta el punto de ser engañosa. Si usa declaraciones preparadas incorrectamente, no es mucho mejor que no usarlas en absoluto. (Por supuesto, una "declaración preparada" a la que se le ha inyectado la entrada del usuario anula el propósito ... pero en realidad lo he visto hecho. Y las declaraciones preparadas no pueden manejar identificadores (nombres de tablas, etc.) como parámetros). Agregar para eso, algunos de los controladores PDO emulan declaraciones preparadas, y hay espacio para que lo hagan incorrectamente (por ejemplo, analizando a medias el SQL). Versión corta: nunca asumas que es así de fácil.
cHao
29

¡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 use MySQL y PDO:

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

Otra cosa que siempre debe 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 solo se trata del lado de la base de datos de las cosas que aún tendría que vigilar cuando muestre los datos. Por ejemplo, utilizando de htmlspecialchars()nuevo con la codificación correcta y el estilo de cita.

PeeHaa
fuente
14

Personalmente, siempre ejecutaría alguna forma de saneamiento en los datos primero, ya que nunca puede confiar en la entrada del usuario, sin embargo, al usar 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 unen. 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 instrucción SQL.

JimmyJ
fuente
1

Eaven, si va a evitar la inyección de sql front-end, utilizando controles html o js, ​​debería considerar que los controles front-end son "anulables".

Puede deshabilitar js o editar un patrón con una herramienta de desarrollo front-end (integrada en Firefox o Chrome actualmente).

Entonces, 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 utilice la función PHP nativa filter_input () para desinfectar los valores GET e INPUT.

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

La seguridad tiene un costo, ¡pero no desperdicies tu rendimiento!

Ejemplo fácil:

si desea verificar dos veces si un valor, recibido de GET es un número, menor que 99 si (! preg_match ('/ [0-9] {1,2} /')) {...} es el más pesado de

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

Entonces, la respuesta final es: "¡No! Las declaraciones preparadas de DOP no evitan todo tipo de inyección sql"; No evita valores inesperados, solo concatenación inesperada

francotirador
fuente
55
Está confundiendo la inyección de SQL con algo más que hace que su respuesta sea completamente irrelevante
su sentido común el