jueves, 18 de julio de 2013

Seguridad PHP - SQL injection

Las aplicaciones web permiten que a través de datos introducidos por los usuarios se realicen consultas y operaciones sobre las bases de datos que integran.
Al intento de  insertar o inyectar código dentro del código SQL para alterar el funcionamiento normal y hacer que se ejecute el código 'malicioso' dentro del sistema se le llama 'inyección de sql' o 'sql injection' (en ingles).
La solución para no permitir la inyección de este código es el correcto tratamiento de los datos que llegan a la aplicación de manera externa. Ya que como hemos dicho anteriormente, nunca hay que fiarse de lo que va hará o no un usuario.

Ejemplos de ataques

A) Acceso de administrador

- La siguiente consulta podría ser una consulta real para averiguar la existencia del usuario introducido en el formulario:

 $usuario = $_POST['usuario'];  
 $pass = $_POST['password'];  
 SELECT * FROM usuarios WHERE usuario = '$usuario' AND password = '$pass';  

Supongamos que dichas variables provienen de los inputs usuario y password del formulario de login y que existe un usuario llamado admin. Que pasaría si el usuario introdujese la cadena admin como login y  'or '1'='1 en la contraseña. Pues que la consulta se evaluaría de la siguiente manera:

 SELECT * FROM usuarios WHERE usuario = 'admin' AND password = '' OR '1'='1';  

Pues sí, podríamos ganarnos el pase como admin!!!. Evidentemente pueden haber variantes en el ataque ya que a parte de que admin puede no existir, puede que en la consulta las variables no estén entre comillas.

- Ataque a la consulta anterior mediante el comentario 'in-line' de SQL. Recordemos que lo que va tras un comentario es ignorado y solo sirve de ayuda o referencia al desarrollador.

Ahora en el input login se introduce admin'-- y como contraseña lo que se quiera. Que cumpla con las reglas del formulario, si las hay. Y la consulta se evaluaría del siguiente modo:

 SELECT * FROM usuarios WHERE usuario = 'admin'--' AND password = 'password'  

Y como lo que hay tras el comentario es ignorado, nuevamente tendriamos acceso de administrador.

B) Borrado de tablas

Un caso más crítico sería en el caso en el que algunas aplicación tienen un input para que introduzcamos un email y así nos envían la contraseña. Que pasaría si en ese input introdujésemos algo como esto prueba@test.com'; DROP TABLE usuarios; --
Evidentemente, depende de como este construida la consulta, pero podría provocar una evaluación parecida a esta:

 SELECT email, passwd, login_id, full_name  
  FROM usuarios  
  WHERE email = 'prueba@test.com'; DROP TABLE usuarios; --';   

Pues parece que se ha borrado la tabla usuarios!!!

Bueno, realmente este caso no nos afectaría ya que MySQLi a partir de php5 no permite ejecutar varias consultas a la vez. A no ser que se utilice la función mysqli_multi_query(). Sin embargo en sistemas on versiones antiguas de PHP es posible aprovecharse de esta circunstancia.

No es el objetivo enseñar ejemplos de todos los ataques, ya que hay miles. Solo decir que no solo pueden ejecutarse en consultas SELECT. También en inserciones o actualizaciones en la base de datos. Y una cosa muy importante es que el atacante se puede aprovechar de la información de error que genera la aplicación para ir poco a poca entendiendo la estructura de la base de datos.

Desactivar magic_quotes

Nota: Por escapar entendemos el colocar el caracter '\' delante de los caracteres problemáticos.
La directiva magic_quotes fue introducida para ayudar a 'escapar' datos automáticamente y evitar problemas de seguridad a la hora de insertarlos en la base de datos. Sin embargo esto supone 3 inconvenientes que han llevado a que esta directiva sea clasificada como obsoleta (PHP 5.3) y eliminada de versiones modernas de PHP.
Los tres principales inconvenientes son los siguientes:
- La idea de 'escapar' automáticamente para curarnos en salud en los datos que vayan a la base de datos puede parecer buena. Pero no todos los datos de entrada al sistema van a parar a la base de datos. Pueden ir a una sesión, mostrarse por pantalla... Y esto puede ocasionar que en ocasiones se muestren datos por pantalla con barras de escapada que no deberían de estar.
- El escapado automático afecta al rendimiento de la aplicación. Por lo que es mucho mejor aplicar escapado cuando sea necesario y decida el desarrollador.
- Problemas de portabilidad. Asumir que todos servidores soportan esta característica es un error ya que en un momento dado puede que tengamos que migrar a un servidor que no la tenga activada. Y eso no lo habíamos tenido en cuenta. Con las consecuencias de seguridad que conlleva.

Además puede crear una falsa ilusión seguridad  ya que no proporciona ningún tipo de seguridad frente a XSS o inyección de cabeceras.

Verificar el tipo de dato como primer paso

La solución para evitar ataques por 'inyección de SQL' pasa por un correcto filtrado de la información proporcionada por el usuario y que va a formar parte de alguna operación con una base de datos. Ya que si de antemano sabemos que han introducido información de un tipo que no corresponde con el esperado, evitamos seguir con el procesado de la información
Para ello podríamos usar tanto los filtros de tipo  FILTER_VALIDATE_... vistos el tutorial 1 de filtrado de datos o las funciones para averiguar el tipo is_int(), is_bool()...del tutorial 2 de filtrado.

Escapar mejor que sanear o eliminar

Hemos presentado en el citado tutorial 1 de filtrado de datos  el filtro FILTER_SANITIZE_STRING. Y puede parecer la solución al tipo de ataques por 'inyección de SQL'. Sin embargo no es totalmente efectivos ya que no se encarga de todos los caracteres maliciosos para este tipo de ataques.
Por lo que buscaremos otra solución para tratar las cadenas introducidas por el usuario. Podríamos pensar entonces en hacer una lista negra de caracteres maliciosos y eliminarlos de las cadenas. Pero nuevamente no es la mejor solución: imagina que estamos ante una aplicación donde el usuario puede dejar comentarios. Que pasaría si introduce comillas para una expresión del comentario o un guión. Pues que si eliminamos caracteres podríamos estar eliminando contenido válido introducido por el usuario.
Entonces la mejor solución pasa por que los caracteres 'maliciosos' sigan en la cadena aportada por el usuario. Pero de manera que no puedan dar problemas. Solución: escapar dichos caracteres.

1. Función para sistemas con versiones antiguas de PHP

La extensión MySQL acaba de quedar obsoleta con la salida PHP 5.5 pero es posible que sea usada en sistemas con versiones antiguas de PHP. En está extensión existe una función encargada de 'escapar' los caracteres especiales de una cadena que se va a usar en una consulta SQL.

 string mysql_real_escape_string ( string $unescaped_string [, resource $link_identifier = NULL ] )  

Observar que el segundo parámetro, el enlace de la conexión con la base de datos, es opcional.
Y como si el sistema usa una versión antigua de PHP podría tener activada la directiva 'magic_quotes', vamos a comprobarlo para que no interfiera con la función de mysql_real_escape_string() mediante el siguiente script.

 function sanearCadena($cadena)  
 {  
   if (get_magic_quotes_gpc())   
     $cadena = stripslashes($cadena);  
     return mysql_real_escape_string($cadena);  
 }  

Pequeña explicación:
- stripslashes() solo se aplica si magic_quotes están activadas (en versión PHP 5.3 estarán desactivadas por defecto). Lo que hacemos es 'desescapar la información' si PHP la ha 'escapada' por tener activa la directiva magic_quotes . Ya que al utilizar mysqli_real_escape_string(), si la magic_quotes están activas, es posible que se produzcan dobles escapados.

2. Interfaz MySQLi

Como ya mostramos en el tutorial de MySQLi, esta extensión ofrece la alternativa de usar la interfaz procedimental y la orientada a objetos. Así que vamos a ver como usar la función de 'escapado' en cada una de ellas.

A) Interfaz orientada a objetos

 string mysqli::real_escape_string ( string $cadenaAEscapar )  

 $mysqli = new mysqli("localhost", "mi_usuario", "mi_contraseña", "demo");  
 if($mysqli->conect_errno) die($mysqli->conect_error);   
 //podemos utililzar el alias mysqli_escape_string 
 $nombre = $mysqli->real_escape_string($_POST['nombre']);  
 $res = $mysqli->query("SELECT * FROM usuario WHERE nombre ='$nombre'");   
 if($res->num_rows == 0) { 
 }  

string real_escape_string(string $cadena) devolverá la misma cadena de entrada pero con los caracteres peligrosos escapados. De esta forma no hay peligro en que se se realizen consultas con estos datos.

B) Interfaz procedimental

 string mysqli_real_escape_string ( mysqli $link , string $cadenaAEscapar )  

Recordar que en la interfaz procedimental hay que especificar el enlace mysqli de la conexión  a la base de datos.

  $con = mysqli_connect("localhost", "mi_usuario", "mi_contraseña", "demo");   
  if(mysqli_connect_errno()) die(mysqli_connect_error());    
  //podriamos utilizar el alias mysqli_escape_string   
  $nombre = mysqli_real_escape_string($con,$_POST['nombre']);   
  $res = mysqli_query($con,"SELECT * FROM usuario WHERE nombre ='$nombre'");   
  if (mysqli_num_rows($res) == 0) {  
   //no encontrado  
  }   


3. PDO

En el tutorial sobre como funciona PDO ya explicamos las ventajas que tenía junto con el uso de las consultas preparadas. Recordemos que al usar las consultas preparadas (prepare() y execute()) estamos separando la lógica de los datos que van a ser enviados al servidor de base de datos. Y ese mecanismo hace que automáticamente sean escapados los datos que vamos a introducir en dichas consultas. Por lo tanto nos hace el trabajo por nosotros y es lo convierte solución más recomendable.
También se puede utilizar PDO sin el uso de consultas preparada. Osea ejecutando consultas mediante la función query(). En este caso, si queremos escapar los valores externos usaremos la función quote().
Aparte tendremos otras ventajas  con el uso de PDO:
- Como mejora de rendimiento en múltiples ejecuciones de consultas similares,
- El uso de transacciones
- Sobre todo la capa de abstracción que ofrece sobre el gestor de base de datos que queramos usar (y que este soportado). Lo cual hará que la codificación será la misma independientemente del tipo de gestor de base de datos con el que estemos tratando. Incrementando de una manera considerable la portabilidad de la aplicación o página web.


Conclusión

No nos podemos fiar de la información que transmitirá el usuario al servidor. Por lo tanto tendremos que procesarla de alguna forma. Lo mejor es 'escaparla' para evitar que caracteres especiales sean aprovechados para ataques sobre la base de datos.
MySQLi ofrece funciones para 'escapar' los datos que vamos a introducir en la base de datos pero lo mejor y más recomendable es usar PDO. Ya que entre otras ventajas, va a 'escapar' los datos por nosotros automáticamente.


Entradas relacionadas

Extensión MySQLi - Parte 3: Consultas preparadas y escapado
Seguridad PHP - Validación y filtrado 1
Seguridad PHP - Validación y filtrado 2
PDO - Parte 1

No hay comentarios:

Publicar un comentario