¿htmlspecialchars y mysql_real_escape_string mantienen mi código PHP a salvo de inyecciones?
Hoy se hizo una pregunta sobre las estrategias de validación de entradas en aplicaciones web .
La respuesta principal, al momento de escribir este artículo, sugiere PHP
simplemente usar htmlspecialchars
y mysql_real_escape_string
.
Mi pregunta es: ¿es esto siempre suficiente? ¿Hay más que deberíamos saber? ¿Dónde se descomponen estas funciones?
Cuando se trata de consultas a bases de datos, intente siempre utilizar consultas parametrizadas preparadas. Las bibliotecas mysqli
y PDO
admiten esto. Esto es infinitamente más seguro que usar funciones de escape como mysql_real_escape_string
.
Sí, mysql_real_escape_string
en realidad es solo una función de escape de cadenas. No es una solución mágica. Todo lo que hará será escapar de los caracteres peligrosos para que sean seguros de usar en una sola cadena de consulta. Sin embargo, si no desinfecta sus entradas de antemano, será vulnerable a ciertos vectores de ataque.
Imagine el siguiente SQL:
$result = "SELECT fields FROM table WHERE id = ".mysql_real_escape_string($_POST['id']);
Debería poder ver que esto es vulnerable a la explotación.
Imagine que el id
parámetro contuviera el vector de ataque común:
1 OR 1=1
No hay caracteres peligrosos para codificar, por lo que pasará directamente a través del filtro de escape. Dejándonos:
SELECT fields FROM table WHERE id= 1 OR 1=1
Que es un hermoso vector de inyección SQL y permitiría al atacante devolver todas las filas. O
1 or is_admin=1 order by id limit 1
que produce
SELECT fields FROM table WHERE id=1 or is_admin=1 order by id limit 1
Lo que permite al atacante devolver los datos del primer administrador en este ejemplo completamente ficticio.
Si bien estas funciones son útiles, deben usarse con cuidado. Debe asegurarse de que todas las entradas web estén validadas hasta cierto punto. En este caso, vemos que podemos ser explotados porque no comprobamos que una variable que estábamos usando como número, en realidad fuera numérica. En PHP deberías utilizar ampliamente un conjunto de funciones para comprobar que las entradas sean enteras, flotantes, alfanuméricas, etc. Pero cuando se trata de SQL, presta más atención al valor de la declaración preparada. El código anterior habría sido seguro si fuera una declaración preparada, ya que las funciones de la base de datos habrían sabido que 1 OR 1=1
no es un literal válido.
Como para htmlspecialchars()
. Ese es un campo minado en sí mismo.
Hay un problema real en PHP ya que tiene una selección completa de diferentes funciones de escape relacionadas con HTML y no hay una guía clara sobre exactamente qué funciones hacen qué.
En primer lugar, si estás dentro de una etiqueta HTML, estás en verdaderos problemas. Mira a
echo '<img src= "' . htmlspecialchars($_GET['imagesrc']) . '" />';
Ya estamos dentro de una etiqueta HTML, por lo que no necesitamos < o > para hacer nada peligroso. Nuestro vector de ataque podría ser simplementejavascript:alert(document.cookie)
Ahora el HTML resultante se ve así
<img src= "javascript:alert(document.cookie)" />
El ataque avanza directamente.
Se pone peor. ¿Por qué? porque htmlspecialchars
(cuando se llama de esta manera) solo codifica comillas dobles y no simples. Así que si tuviéramos
echo "<img src= '" . htmlspecialchars($_GET['imagesrc']) . ". />";
Nuestro malvado atacante ahora puede inyectar parámetros completamente nuevos
pic.png' onclick='location.href=xxx' onmouseover='...
Nos da
<img src='pic.png' onclick='location.href=xxx' onmouseover='...' />
En estos casos, no existe una solución mágica, sólo tienes que sanear la entrada tú mismo. Si intentas filtrar los personajes malos, seguramente fracasarás. Adopte un enfoque de lista blanca y solo deje pasar los caracteres que sean buenos. Mire la hoja de trucos XSS para ver ejemplos sobre cuán diversos pueden ser los vectores.
Incluso si utiliza htmlspecialchars($string)
etiquetas fuera de HTML, sigue siendo vulnerable a vectores de ataque de conjuntos de caracteres multibyte.
Lo más eficaz que puede ser es utilizar una combinación de mb_convert_encoding y htmlentities de la siguiente manera.
$str = mb_convert_encoding($str, 'UTF-8', 'UTF-8');
$str = htmlentities($str, ENT_QUOTES, 'UTF-8');
Incluso esto deja a IE6 vulnerable, debido a la forma en que maneja UTF. Sin embargo, puede recurrir a una codificación más limitada, como ISO-8859-1, hasta que disminuya el uso de IE6.
Para un estudio más profundo de los problemas multibyte, consulte https://stackoverflow.com/a/12118602/1820
Además de la excelente respuesta de Cheekysoft:
- Sí, te mantendrán seguro, pero sólo si se usan de forma absolutamente correcta. Úselos incorrectamente y seguirá siendo vulnerable y puede tener otros problemas (por ejemplo, corrupción de datos)
- Utilice consultas parametrizadas en su lugar (como se indicó anteriormente). Puede usarlos, por ejemplo, a través de PDO o mediante un contenedor como PEAR DB
- Asegúrate de que magic_quotes_gpc y magic_quotes_runtime estén apagados en todo momento y nunca se enciendan accidentalmente, ni siquiera brevemente. Estos son un intento temprano y profundamente equivocado por parte de los desarrolladores de PHP para evitar problemas de seguridad (que destruyen datos).
Realmente no existe una solución milagrosa para prevenir la inyección de HTML (por ejemplo, secuencias de comandos entre sitios), pero es posible que pueda lograrlo más fácilmente si está utilizando una biblioteca o un sistema de plantillas para generar HTML. Lea la documentación sobre cómo escapar de las cosas de manera adecuada.
En HTML, las cosas deben escaparse de manera diferente según el contexto. Esto es especialmente cierto en el caso de cadenas que se colocan en Javascript.
Definitivamente estaría de acuerdo con las publicaciones anteriores, pero tengo una pequeña cosa que agregar en respuesta a la respuesta de Cheekysoft, específicamente:
Cuando se trata de consultas a bases de datos, intente siempre utilizar consultas parametrizadas preparadas. Las bibliotecas mysqli y PDO lo admiten. Esto es infinitamente más seguro que usar funciones de escape como mysql_real_escape_string.
Sí, mysql_real_escape_string es efectivamente solo una función de escape de cadena. No es una solución mágica. Todo lo que hará será escapar de los caracteres peligrosos para que sean seguros de usar en una sola cadena de consulta. Sin embargo, si no desinfecta sus entradas de antemano, será vulnerable a ciertos vectores de ataque.
Imagine el siguiente SQL:
$resultado = "SELECCIONE campos DE la tabla DONDE id = ".mysql_real_escape_string($_POST['id']);
Debería poder ver que esto es vulnerable a la explotación. Imagine que el parámetro id contuviera el vector de ataque común:
1 O 1=1
No hay caracteres peligrosos para codificar, por lo que pasará directamente a través del filtro de escape. Dejándonos:
SELECCIONE campos DE la tabla DONDE id = 1 O 1 = 1
Codifiqué una pequeña función rápida que puse en mi clase de base de datos y que eliminará todo lo que no sea un número. Utiliza preg_replace, por lo que probablemente haya una función un poco más optimizada, pero funciona en caso de necesidad...
function Numbers($input) {
$input = preg_replace("/[^0-9]/","", $input);
if($input == '') $input = 0;
return $input;
}
Entonces en lugar de usar
$resultado = "SELECCIONE campos DE la tabla DONDE id = ".mysqlrealescapestring("1 O 1=1");
yo usaría
$resultado = "SELECCIONE campos DE la tabla DONDE id = ".Numbers("1 O 1=1");
y ejecutaría la consulta de forma segura
SELECCIONE campos DE la tabla DONDE id = 111
Claro, eso simplemente impidió que mostrara la fila correcta, pero no creo que sea un gran problema para quien esté intentando inyectar SQL en su sitio;)
Una pieza importante de este rompecabezas son los contextos. Alguien que envíe "1 OR 1=1" como ID no es un problema si cita todos los argumentos de su consulta:
SELECT fields FROM table WHERE id='".mysql_real_escape_string($_GET['id'])."'"
Lo que resulta en:
SELECT fields FROM table WHERE id='1 OR 1=1'
lo cual es ineficaz. Dado que está escapando de la cadena, la entrada no puede salir del contexto de la cadena. He probado esto hasta la versión 5.0.45 de MySQL y el uso de un contexto de cadena para una columna de números enteros no causa ningún problema.