php - proteger - ¿Son las declaraciones preparadas PDO suficientes para evitar la inyección SQL?
proteger php contra sql injection (7)
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:
Los parámetros para las declaraciones preparadas no necesitan ser citados; El conductor lo maneja por ti.
¿Es realmente todo lo que necesito hacer para evitar las inyecciones de SQL? ¿Es realmente tan fácil?
Puedes asumir MySQL si hace una 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 posibles vulnerabilidades.
Incluso si va a evitar la inyección de SQL front-end, utilizando html o js check, tendría que considerar que los front-end están "anulables".
Puede deshabilitar js o editar un patrón con una herramienta de desarrollo de aplicaciones para 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 back-end de la fecha de entrada dentro de su controlador.
Me gustaría sugerirle que use la función PHP nativa filter_input () para sanear los valores GET e INPUT.
Si desea continuar con la seguridad, para consultas de bases de datos razonables, 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 volver a verificar si un valor recibido de GET es un número, menos de 99 si (! preg_match (''/ [0-9] {1,2} /'')) {...} es más pesado de
if (isset($value) && intval($value)) <99) {...}
Entonces, la respuesta final es: "¡No! Las declaraciones preparadas de DOP no impiden todo tipo de inyección de sql"; No evita valores inesperados, solo concatenación inesperada.
La respuesta corta es NO , los preparativos de PDO no lo defenderán de todos los posibles ataques de inyección de SQL. Para ciertos casos de borde 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, vamos a empezar 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 analizar lo que está pasando aquí:
Selección de 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
y tener algún carácter cuyo byte final sea un ASCII, es decir,0x5c
. Como resultado, hay 5 codificaciones de este tipo admitidas en MySQL 5.6 por defecto:big5
,cp932
,gb2312
,gbk
ysjis
. Seleccionaremosgbk
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.La carga útil
La carga útil que vamos a utilizar para esta inyección comienza con la secuencia de bytes
0xbf27
. Engbk
, ese es un carácter multibyte no válido; enlatin1
, es la cadena¿''
. Tenga en cuenta que engbk
ygbk
,0x27
en sí mismo es un carácter literal''
.Hemos elegido esta carga útil porque, si
addslashes()
en ella, insertaríamos un ASCII/
ie0x5c
, antes del carácter''
. Así que terminamos con0xbf5c27
, que engbk
es una secuencia de dos caracteres:0xbf5c
seguido de0x27
. O, en otras palabras, un carácter válido seguido de un''
sin escape''
. Pero no estamos utilizandoaddslashes()
. Así que en el siguiente paso ...$ stmt-> ejecutar ()
Lo importante a tener en cuenta aquí es que, de manera predeterminada, la DOP NO hace declaraciones verdaderas 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 deaddslashes()
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 estamos usandolatin1
para la conexión, porque nunca le dijimos lo contrario. Le dijimos al servidor que estamos usandogbk
, pero el cliente aún piensa que eslatin1
.Por lo tanto, la llamada a
mysql_real_escape_string()
inserta la barra invertida, ¡y tenemos un carácter colgado en nuestro contenido "escapado"! De hecho, sigbk
a$var
en elgbk
caracteresgbk
, veríamos:縗'' OR 1=1 /*
Que es exactamente lo que requiere el ataque.
La consulta
Esta parte es solo una formalidad, pero aquí está la consulta renderizada:
SELECT * FROM test WHERE name = ''縗'' OR 1=1 /*'' LIMIT 1
Enhorabuena, acaba 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);
Por lo general, esto resultará en una verdadera declaración preparada (es decir, los datos se envían en un paquete separado de la consulta). Sin embargo, tenga en cuenta que la DOP fallback silenciosamente para emular las declaraciones que MySQL no puede preparar de forma nativa: las que pueden listed en el manual, pero tenga cuidado de seleccionar la versión de servidor apropiada).
La corrección correcta
El problema aquí es que no llamamos mysql_set_charset()
la API C en lugar de SET NAMES
. Si lo hiciéramos, estaríamos bien siempre que estemos usando una versión de MySQL desde 2006.
Si está utilizando una versión anterior de MySQL, un bug en mysql_real_escape_string()
significaba que los caracteres multibyte no válidos, como los de nuestra carga útil, se trataron como bytes individuales para fines de escape, incluso si el cliente había sido informado correctamente de la codificación de la conexión y Este ataque todavía tendría é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 expuso la API de C para mysql_set_charset()
hasta 5.3.6, por lo que en versiones anteriores no puede evitar este ataque para cada comando posible! Ahora se expone como un parámetro DSN , que se debe usar 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 vulnerables. utf8mb4
no es vulnerable y, sin embargo, puede admitir todos los caracteres de Unicode: así que puede elegir usar eso en su lugar, pero solo está disponible desde MySQL 5.5.3. Una alternativa es utf8
, que tampoco es vulnerable y puede admitir todo el plano utf8
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 seguirá 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 establecido correctamente el conjunto 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:
- Use las versiones modernas de MySQL (finales de 5.1, todas las 5.5, 5.6, etc.) Y el parámetro del conjunto 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 use
utf8
/utf8
/ascii
/ etc)
O
- Habilitar
NO_BACKSLASH_ESCAPES
modo SQL
Estás 100% seguro.
De lo contrario, es vulnerable a pesar de que está utilizando declaraciones preparadas para DOP ...
Apéndice
He estado trabajando lentamente en un parche para cambiar el valor predeterminado para no emular los preparativos para una versión futura de PHP. El problema con el que me estoy topando es que muchas de las pruebas se rompen cuando lo hago. Un problema es que las preparaciones emuladas solo generarán errores de sintaxis en la ejecución, pero las preparaciones verdaderas generarán errores en la preparación. Así que eso puede causar problemas (y es parte de la razón por la que las pruebas son falsas).
Las declaraciones preparadas / consultas parametrizadas son generalmente suficientes para evitar la inyección de primer orden en esa declaración * . Si usa un sql dinámico sin comprobar en cualquier otra parte de su aplicación, todavía 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 se ven ataques de 2º orden diseñados con ingeniería real, ya que usualmente es más fácil para los atacantes hacer ingeniería social, pero a veces surgen errores de 2º orden debido a personajes 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, supongamos 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 se asegurará 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 pueda 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 de servidores. (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 de SQL en toda la aplicación, ya que carecen de un mecanismo para imponer que todo el acceso a una base de datos dentro de la aplicación lo use. código de seguridad 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 sentencias preparadas en sql dinámico: la herramienta principal para resolver la Inyección de SQL. problema. Si sigue buenos principios de diseño de aplicaciones, de manera que su acceso a los datos está separado del resto de su programa, es fácil hacer cumplir o auditar que cada consulta utiliza correctamente la parametrización. En este caso, la inyección de sql (tanto de primer como de segundo orden) se evita por completo.
* Resulta que MySql / PHP son (bueno, eran) tontos acerca del manejo de parámetros cuando se utilizan 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.
No, esto no es suficiente (en algunos casos específicos)! De forma predeterminada, PDO utiliza declaraciones preparadas emuladas cuando se utiliza MySQL como controlador de base de datos. Siempre debe desactivar las declaraciones 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 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 htmlspecialchars()
nuevo con la codificación y el estilo de cita correctos.
No, no lo son siempre.
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 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 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 enlazar 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 DESC
y ASC
no son datos . DOP solo puede escapar de los datos . En segundo lugar, ni siquiera puedes poner ''
comillas ''
a su alrededor. La única forma de permitir que el usuario elija la clasificación es filtrar manualmente y verificar que sea DESC
o ASC
.
Personalmente, siempre ejecutaría algún tipo de saneamiento en los datos primero, ya que nunca se puede confiar en la entrada del usuario, sin embargo, al usar marcadores de posición / parámetros, los datos ingresados se envían al servidor por separado a la declaración de SQL y luego se unen. La clave aquí es que esto une 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.
Sí, es suficiente. La forma en que funcionan los ataques de tipo de inyección es obtener un intérprete (la base de datos) para evaluar algo, que debería haber sido información, 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 al enviar el código y los datos por separado, por lo que nunca sería posible encontrar un agujero en eso.
Sin embargo, todavía puedes 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.