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í:
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
, gbk
y sjis
. Seleccionaremos gbk
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
. En gbk
, ese es un carácter multibyte no válido; adentro latin1
, es la cuerda ¿'
. Tenga en cuenta que en latin1
y gbk
, 0x27
por 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 gbk
es una secuencia de dos caracteres: 0xbf5c
seguida 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 ...
$ 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 latin1
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()
insertar la barra diagonal inversa, ¡y tenemos un '
personaje que cuelga libremente en nuestro contenido "escapado"! De hecho, si tuviéramos que mirar $var
en el gbk
conjunto de caracteres, veríamos:
OR 'OR 1 = 1 / *
Que es exactamente lo que requiere el ataque.
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 PDO
no 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_ESCAPES
modo SQL, que (entre otras cosas) altera el funcionamiento de mysql_real_escape_string()
. Con este modo habilitado, 0x27
será reemplazado por 0x2727
algo en 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 ( 0xbf27
es decir, todavía es 0xbf27
etc.), 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_ESCAPES
modo 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).