¿Son las declaraciones preparadas PDO suficientes para prevenir la inyección 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 la DOP dice:

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

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

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

Author: Patrick Hofman, 2008-09-25

7 answers

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

Estoy adaptandoesta 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 ciertas circunstancias, eso devolverá más de 1 fila. Vamos a diseccionar lo que está pasando 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 i. e.0x27 y tener algún carácter cuyo byte final es un ASCII \ es decir, 0x5c. Resulta que hay 5 tales codificaciones soportadas en MySQL 5.6 por defecto: big5, cp932, gb2312, gbk y sjis. Vamos a seleccionar gbk aqui.

    Ahora, es muy importante notar el uso de SET NAMES aquí. Esto establece el conjunto de caracteres EN EL SERVIDOR. Hay otra forma de hacerlo, pero lo conseguiremos 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, es un carácter multibyte no válido; en latin1, es la cadena ¿'. Tenga en cuenta que en latin1 y gbk, 0x27 por su cuenta es un carácter literal '.

    Hemos elegido esta carga porque, si llamamos addslashes() en ella, insertaríamos un ASCII \, es decir, 0x5c, antes del carácter '. Así que terminaríamos con 0xbf5c27, que en gbk es una secuencia de dos caracteres: 0xbf5c seguido de 0x27. O en otras palabras, un carácter válido seguido de un ' sin escapatoria. Pero no estamos usando addslashes(). Así que al siguiente paso...

  3. $stmt- > ejecutar()

    Lo importante lo que hay que tener en cuenta aquí es que PDO por defecto hace NO hacer 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 la API C a mysql_real_escape_string() difiere de addslashes() en que conoce el conjunto de caracteres de conexión. Por lo que puede realizar el escape correctamente para el conjunto de caracteres que el servidor está esperando. Sin embargo, hasta este punto, el cliente piensa que estamos seguimos 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 invertida, y tenemos un carácter de suspensión libre ' en nuestro contenido "escapado"! De hecho, si tuviéramos que mirar $var en el conjunto de caracteres gbk, veríamos:

    縗' OR 1=1 /*

    Que es exactamente lo que requiere el ataque.

  4. Las Query

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

    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 Simple

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

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

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

La Solució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 y cuando estemos usando una versión de MySQL desde 2006.

Si está utilizando una versión anterior de MySQL, entonces una el error en mysql_real_escape_string() significaba que los caracteres multibyte no válidos, como los de nuestra carga útil, se trataban como bytes individuales para fines de escape incluso si el cliente había sido informado correctamente de la codificación de conexión y, por lo tanto, este ataque seguiría teniendo éxito. El error fue corregido en MySQL 4.1.20, 5.0.22 y 5.1.11.

Pero la peor parte es que PDO no expuso la API de C para mysql_set_charset() hasta la versión 5.3.6, por lo que en versiones anteriores no puede prevenir este ataque por cada orden 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 a la base de datos debe codificarse utilizando un conjunto de caracteres vulnerable. utf8mb4 es no vulnerable y, sin embargo, puede soportar cada carácter Unicode: por lo que podría elegir usar eso en su lugar,pero tiene solo está disponible desde MySQL 5.5.3. Una alternativa es utf8, que también es no vulnerable y puede soportar la totalidad del Plano Multilingüe Básico Unicode .

Alternativamente, puede habilitar el NO_BACKSLASH_ESCAPES Modo SQL, que (entre otras cosas) altera el funcionamiento de mysql_real_escape_string(). Con este modo habilitado, 0x27 se reemplazará por 0x2727 en lugar de 0x5c27 y, por lo tanto, el proceso de escape no puede crear caracteres válidos en cualquier de las codificaciones vulnerables donde no existían previamente (es decir, 0xbf27 sigue siendo 0xbf27 etc.)- por lo que el servidor rechazará la cadena como no vá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 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 establecido correctamente el conjunto de caracteres el cliente y el servidor coinciden.

$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 usted:

  • Use versiones modernas de MySQL (finales de 5.1, todas 5.5, 5.6, etc.) Y Parámetro de conjunto de caracteres DSN de PDO (en PHP ≥ 5.3.6)

O

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

O

  • Habilitar NO_BACKSLASH_ESCAPES Modo SQL

Estás 100% a salvo.

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

Adición

He estado trabajando lentamente en un parche para cambiar el valor predeterminado para no emular se prepara para una versión futura de PHP. El problema que soy encontrarme es que muchas pruebas se rompen cuando hago eso. Un problema es que emulated prepares solo lanzará errores de sintaxis en execute, pero true prepares lanzará errores en prepare. Por lo que puede causar problemas (y es parte de la razón por la que las pruebas están borking).

 702
Author: ircmaxell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:34:48

Las sentencias preparadas / consultas parametrizadas son generalmente suficientes para evitar inyección de 1er orden en esa sentencia*. Si utiliza sql dinámico no comprobado en cualquier otro lugar de su aplicación, todavía es vulnerable a inyección de 2do orden.

La inyección de 2do orden significa que los datos han sido ciclados 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 realizar. AFAIK, casi nunca se ven ataques reales de 2da orden diseñados, como es por lo general, es más fácil para los atacantes a la ingeniería social de su camino en, pero a veces tiene errores de 2do orden surgen debido a caracteres ' extra benignos o similares.

Puede realizar un ataque de inyección de 2do orden cuando puede hacer que un valor se almacene en una base de datos que luego se use como un literal en una consulta. Como ejemplo, supongamos que introduce 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 instrucción preparada todavía se asegurará de que la consulta incrustada anterior no se ejecute en el momento de insertar, y almacenará 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 usa 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 dado lejos de la granja. (También nota: esta es una razón más para no almacenar contraseñas en texto plano!)

Vemos, entonces, que las sentencias preparadas son suficientes para una sola consulta, pero por sí mismas son no suficientes para proteger contra ataques de inyección sql en toda una aplicación, porque carecen de un mecanismo para imponer que todo el acceso a una base de datos dentro de la aplicación use código seguro. Sin embargo, se utiliza como parte del buen diseño de la aplicación , que puede incluir prácticas como revisión de código o análisis estático, o uso de un OR, capa de datos o capa de servicio que limita sql dinámico - declaraciones preparadas son la herramienta principal para resolver el problema de la inyección Sql. Si sigue buenos principios de diseño de aplicaciones, tales que su acceso a los datos está separado del resto de su programa, se vuelve fácil hacer cumplir o auditar que cada consulta utiliza correctamente la parametrización. En este caso, la inyección sql (tanto de primer como de segundo orden) es completamente prevenir.


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

 496
Author: Joel Coehoorn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-18 16:53:12

No, no siempre lo son.

Depende de si permite que la entrada del usuario se coloque dentro de la propia consulta. 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 SQL y el uso de instrucciones 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 se puede usar PDO para enlazar datos que van fuera de DDL (Data Definition Language), 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 DESC y ASC no son datos. La DOP solo puede escapar para datos . En segundo lugar, ni siquiera puedes poner ' comillas a su alrededor. La única manera de permitir la clasificación elegida por el usuario es filtrar manualmente y verificar que sea DESC o ASC.

 40
Author: Tower,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-09-27 13:20:25

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 solo es posible si mezcla 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.

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

 24
Author: troelskn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-25 15:55:46

No esto no es suficiente (en algunos casos específicos)! De forma predeterminada, PDO utiliza instrucciones preparadas emuladas cuando se utiliza MySQL como controlador de base de datos. Siempre debe deshabilitar las sentencias preparadas emuladas cuando use 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');

También vea esta pregunta relacionada: ¿Cómo puedo prevenir la inyección SQL en PHP?

También tenga en cuenta que solo se trata del lado de la base de datos de la cosas que todavía tendría que ver a sí mismo al mostrar los datos. Por ejemplo, usando htmlspecialchars() de nuevo con la codificación correcta y el estilo de comillas.

 24
Author: PeeHaa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:34:48

Personalmente siempre ejecutaría alguna forma de saneamiento en los datos primero, ya que nunca se puede confiar en la entrada 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 instrucción sql y luego se vinculan juntos. 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.

 9
Author: JimmyJ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-25 15:50:43

Eaven si va a evitar el front-end de inyección sql, utilizando comprobaciones html o js, tendrá que considerar que las comprobaciones del front-end son "bypassable".

Puede deshabilitar js o editar un patrón con una herramienta de desarrollo front-end (incorporada con firefox o chrome hoy en día).

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

Me gustaría sugerirte que uses la función nativa de PHP filter_input() para desinfecte los valores GET e INPUT.

Si desea seguir adelante con la seguridad, para consultas de base de datos sensibles, 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 ligero. Úselo solo si es necesario, de lo contrario el rendimiento de su aplicación disminuirá.

La seguridad tiene un costo, pero no pierda su rendimiento!

Ejemplo sencillo:

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

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

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

 -2
Author: snipershady,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-04 20:17:56