Buenas a todos, por petición expresa de @Zipus hoy vamos a dedicar la entrada a las inyecciones SQL, hablaremos sobre qué son, cómo funcionan y cómo deben filtrarse en PHP.
Hoy en día las inyecciones SQL no son nada nuevo, pero si que hay aún numerosos desarrolladores que las desconocen, mismamente la semana pasada lo discutía con un conocido, desarrollador en una conocida empresa, el cual no solo desconocía ésta vulnerabilidad, si no también otras como los XSS, XPath Injection, Blind, etc. Pero por desgracia, el hecho de este compañero del sector no es un hecho aislado, y así lo certifica la última actualización del documento generado por SANS y MITRE, donde analizaba los 25 errores de programación más peligrosos cometidos por los desarrolladores:
http://cwe.mitre.org/top25/archive/2010/2010_cwe_sans_top25.pdf
En el documento comentan que los XSS encabezan la lista de los errores más comunes, seguidos inmediatamente en segundo lugar por las SQL Injection. Por lo que parece que todavía no existe demasiada concienciación sobre el tema ¿no pensáis?
Comencemos comentando que son las SQL Injection para los begginers en el tema. Pongamos como ejemplo que tenemos un portal Web en el que hay una sección privada para la que se requiere una autenticación mediante usuario y contraseña. El portal Web, programado en PHP tira contra una BBDD MySql donde se encuentran almacenadas en una tabla llamada "T_usuarios" todos los usuarios administradores del sistema con su contraseña. Por lo que tendríamos un escenario como el siguiente, en el que se supone que el sistema tiene un administrador llamado "Carlos" con la contraseña "123abc.":
Imaginemos ahora que tenemos en la página PHP de autenticación la siguiente query con la que intentaremos recuperar los usuarios que coinciden con el usuario y contraseña proporcionados por el usuario:
query = "SELECT * FROM T_usuarios WHERE nombre = '" . $nombreUsuario . "' and password = '" . $passwordUsuario . "'";Donde las variables nombreUsuario y passwordUsuario contienen el valor introducito en los siguientes textbox:
<form id="form1" name="form1" method="post" action="login.php">User: <input type="text" name="user" id="user">Pass: <input type="password" name="pass" id="pass"><input type="submit" name="aceptar" value="Log in"></form>Pongamos ahora que un usuario malicioso inserta el siguiente valor en cada cajetin del textbox:
' or '1'='1
Ocurriendo lo siguiente:Es decir la query quedaría de la siguiente manera:query = "SELECT * FROM T_usuarios WHERE nombre = '' or '1' = '1' and password = '' or '1' = '1';
Con esto estamos diciéndole a la BBDD que queremos que nos devuelva todos los usuarios cuyo nombre sea igual a '', es decir, a vacío, esta sentencia nunca ocurrirá porque no existen usuarios con nombre vacío en la BBDD. La siguiente sentencia "or '1'='1', está anulando a la sentencia anterior gracias al operador "or", y dando el poder a la siguiente afirmación, que está indicando una sentencia que se va a cumplir siempre, que 1 es igual a 1. Lo mismo ocurre con la password.
En resumen, vamos a seleccionar todos los usuarios de la tabla "T_usuarios" Sí SU nombre es igual a vacío ó SÍ 1=1, y como la segunda sentencia es verdadera siempre, devolverá TODOS los usuarios de la BBDD. ¿Correcto no?
Por lo que si nuestro sistema de autenticación en el código PHP es algo tan simple como lo siguiente (donde no importa ni el tipo de usuario), nos habremos colado en el sistema:
$query = "SELECT * FROM T_usuarios WHERE nombre='".($user)."' AND password='".($pass)."'";$resultado = mysql_query($query);if(mysql_fetch_row($resultado)){$_SESSION['conectado']=1;}
Ahora pongamos otro caso, imaginemos que el desarrollador "es zurdo" y le gusta "programar las cosas de izquierdas" =)
query = "SELECT * FROM T_usuarios WHERE '" . $nombreUsuario . "' = nombre and '" . $passwordUsuario . "' = password";
Pues de la misma manera intentamos inutilizar las igualdades de la variable nombreUsuario con la columna nombre y de la variable passwordUsuario con la columna passwrord con la siguiente query:
1' = '1' or '
Otro ejemplo interesante y muy ilustrativo es el siguiente. Imaginemos que un usuario mete en el cajetín del usuario cualquier cosa y en el cajetin de la password la siguiente query:algo';DROP TABLE T_usuarios;SELECT password FROM T_clientes where nombre='Carlos
En primer lugar se ejecutaría la selección del usuario para el login como ya se ha visto antes, en segundo lugar eliminaría la tabla T_usuarios, y en tercer lugar (y por finalizar la query para que no de error porqué quedó una comilla suelta. También podríamos haber comentado el resto del código con "--") seleccionaría, por ejemplo, la contraseña del cliente Carlos:
query = "SELECT * FROM T_usuarios WHERE nombre = 'algo' and password = 'algo';DROP TABLE T_usuarios;SELECT password FROM T_clientes where nombre='Carlos';
Pero no todo en las inyecciones SQL son las comillas, imaginamos que tenemos la siguiente query:
query = "SELECT * FROM T_usuarios WHERE id=" . $idUsuario;
El programador está suponiendo que la variable $idUsuario va a contener un número entero con el id de un usuario. Pero alguien podría insertar algo como lo siguiente:
1 or '1'='1'
Quedando la siguiente query:
query = "SELECT * FROM T_usuarios WHERE id=1 or '1'='1'";
Es decir, se seleccionarían todos los usuarios SI el id=1 o SI 1=1, y como la segunda sentencia es verdadera, devolverá todos los usuarios de la BBDD.
Ahora bien, ¿qué podemos hacer los desarrolladores para protegernos?
De una manera estándar a todos los lenguajes la solución sería validar SIEMPRE los datos de entrada y parametrizar las sentencias SQL, para que los trozos de código insertados por los usuarios no sean interpretados como código, si no únicamente tomados como el tipo de datos (números o letras) que esperamos que el usuario introduzca. Si por ejemplo queremos que el usuario introduzca un id numérico, podríamos "castear" la variable donde se asigna el texto del cajetin del textbox con un (int) para así verificar que no podrá meter texto.
Por ejemplo, para el caso concreto de PHP y MySql, como nos proponía @zipus, se utilizaría la siguiente función, que coloca barras invertidas antes de los siguientes caracteres para evitar la inyección: \x00, \n, \r, \,', " y \x1a:
mysql_real_escape_string($variable)
Quedando la query de la siguiente manera:$query = "SELECT * FROM T_usuarios WHERE nombre='".mysql_real_escape_string($user)."' AND password='".mysql_real_escape_string($pass)."'";
Hace un tiempo, hasta PHP 5.3.0 si no me equivoco, existía una funcionalidad llamada magic quotes, que se encargaba de poner delante de las comillas simples y dobles una barra invertida, y así evitar la inyección. Esta operación era automática, sin necesidad de llamar a ninguna función, por lo que no era necesario llamar manualmente a la función addslashes() para anteponer la barra invertida a una comilla. Sin embargo, no ha sido muy querida esta funcionalidad, ya que si portas la aplicación PHP a otro server con otra configuración de las magic quotes puedes tener problemas. Y al revés, si controlas manualmente las comillas con addslashes() y portas la aplicación a un server con las magic quotes, podrían darse casos de que antes de una comilla se pusiesen dos barras invertidas, lo que podría hacer que se guardase un dato en la BBDD como \'.
Así que una manera de filtrar más estándar podría ser la siguiente, bloqueando el daño que puedan causar las magic quotes:
function filtrar($variable){ // Este if se encargará de retirar las barras en caso de que las comillas mágicas estén habilitadas if (get_magic_quotes_gpc()) { $variable = stripslashes($variable); } if (!is_numeric($variable)) { $variable= "'" . mysql_real_escape_string($variable) . "'"; } return $variable;}
NOTA: get_magic_quotes_gpc devuelve 1 si las magic_quotes están habilitadas y 0 si no lo están. En caso de que estén habilitadas, automáticamente se coloca una barra invertida \ antes de una comilla simple, doble comilla, nulos, etc. Es decir, que si después hiciesemos un mysql_real_escape_string, estaríamos poniendo dos barras invertidas \\ antes de las camillas simples, dobles, nulos, etc. Por ello utilizamos la función stripslashes() que elimina las primeras barras invertidas que hayan podido poner las magic_quotes, para que solo se coloquen las de mysql_real_escape_string().
Nos quedaría la query de la siguiente manera:$query = "SELECT * FROM T_usuarios WHERE nombre='".filtrar($user)."' AND password='".filtrar($pass)."'";
Para despedirme os dejo con éste enlace que me pasó hace algún tiempo Oca con algunas inyecciones curiosas:
http://websec.wordpress.com/2010/03/19/exploiting-hard-filtered-sql-injections/
Eso es todo por hoy, espero que os haya gustado el post.
Saludos!