Bienvenido a mi sitio. Como te habrás dado cuenta, mi nombre es Bruno y soy un desarrollador web que vive en Buenos Aires, Argentina. Este es un espacio para compartir ideas de todo tipo, sentite libre de opinar. Cualquier cosa que quieras decirme, escribime.

Vulnerabilidades en la funcion mail() de PHP

2007.05.03

Cualquiera que tenga una cuenta de email puede asegurar que el spam es como un cáncer de internet y que no beneficia a nadie. Uno se imagina servidores lejanos, en China o Singapur, que es donde sale la mayor parte del spam, sin embargo, no son los únicos a los que debemos culpar. Puede estar incluso en tu propio servidor. Existe mucho código PHP (y en otros lenguajes quizás más) inseguro que provee oportunidades a spammers de abusar los recursos del otro para enviar su correo basura. En particular es la función mail() la que puede ser abusada. No es tan trivial protegerse como podría parecer en un principio.

Este artículo justamente se trata de entender esta vulnerabilidad y ver distintas técnicas que pueden ser utilizadas para mejorar los formularios de contacto, reduciendo así las posibilidades que algunas personas lo usen para enviar spam. Tengo que confesar que tuve muchas dudas sobre si publicar este artículo o no. La idea no es “avivar giles” para que empiecen a probar las cosas que comento, sino ayudar a ciertos desarrolladores y webmasters a que le presten más atención a la seguridad de sus formularios — por el bien de ellos y de todos.

¿Cómo te encuentra un spammer?

Básicamente, hay dos formas de que te encuentren. La más obvia es que visiten tu sitio y naveguen a través de los enlaces hasta llegar a un formulario de contacto público. No hay mucho que puedas hacer para prevenir esto. La otra forma, que es sorprendentemente común, es que lleguen a través de los buscadores como Google o Yahoo!. Buscando los términos mail.php, contacto.php y cosas por el estilo, los spammers tienen una larguísima lista de posibles objetivos. Podés evitar estar en estas listas comunes poniendo un nombre inusual a tu archivo o sino modificando el archivo robots.txt para que no indexen esta parte de tu sitio.

Este tipo de seguridad por ocultamiento no está ni siquiera cerca de ser suficiente, pero si lo pensás un segundo, harán que te encuentren menos, lo que significa menos intentos de spammear, y finalmente se deriva en una chance más chica de tener un exploit.

Un ejemplo para entender mejor

Acá hay un simple formulario HTML utilizado para recibir comentarios y redireccionarlos a un email, seguido por el pedazo de código en PHP utilizado para enviarlo realmente. Está reducido al mínimo posible (esto no respeta estándares) por ahora para hacerlo más simple:

form.html (1)


<form action="mail.php" method="post">
   Email: <input type="text" name="email" value="">
   Mensaje: <textarea name="mensaje"></textarea>
   <input type="submit" value="Envie su mensaje">
</form>

mail.php (1)


<?php
$para = "pepe@dominio.com";
$asunto = "Email del website";
$mensaje = $_REQUEST['mensaje'];
$email = $_REQUEST['email'];

$headers = "From: $email";
mail($para, $asunto, $mensaje, $headers);
echo "Gracias por su mensaje.";
?>

El resultado debería verse similar a esto (en cualquier cliente de correo es la información básica que aparece):


De: remitente@tu-dominio.com
Asunto: Email del website
Para: pepe@dominio.com

Esta combinación de formulario y código es spam a punto de suceder. Fijate que la dirección de correo al que le mandamos el email está fija (o hardcodeada, para los amigos :P). Muchas aplicaciones o formularios todavía son más laxos y le permiten al usuario especificar la dirección de email a la que le quieren mandar el mensaje. Claro que todavía esto hace las cosas más fáciles para los spammers. El problema en esto está en suponer inocentemente que hardcodear el destinatario es suficiente para evitar el spam. Esto es claramente falso. El script anterior usa $_REQUEST, que acepta tanto $_POST como $_GET. Sería mejor armar el formulario con $_POST, pero para motivos ilustrativos, usé $_REQUEST. Además, el método post solo resulta en una demora insignificante para un spammer. El método get solo lo hace más rápido y obvio de atacar.

Atacando el código

Asumiendo que el código y el formulario de arriba están ubicados en un servidor llamado tu-dominio.com, los spammers pueden mandar la siguiente petición:

http://tu-dominio.com/mail.php?mensaje=gotcha&email=hola@dominio-falso.com%0Abcc:spam-1@undominio.com,spam2@un-dominio.com

Recordá, por motivos ilustrativos lo paso por get, pero atacar por post solo sería unos segundos de demora y para nada más complicado.

Miremos esto con un poco más de detalle. Los nombres de las variables están disponibles en el formulario, así que son públicas para los spammers y ellos saben que tienen que usar mensaje e email. El mensaje tendrá el contenido del mail (en este caso, una sola palabra “gotcha”), aunque claro, en la práctica sería algo que promocione viagra o similar.

Luego de eso viene la parte tramposa. El campo email, que supuestamente solo contiene la dirección de email del remitente (en este caso se usa hola@dominio-falso.com), también contiene un campo bcc, seguido por una lista delimitada por comas de direcciones de email para ser spammeadas. El carácter %0A es el código de una nueva línea.

Entonces, con estos parámetros, la cabecera del email se vería similar a esta:


De: pepe@dominio.com
Asunto: Email del website
Para: remitente@tu-dominio.com
Bcc: spam-1@undominio.com,spam2@un-dominio.com

Sin ninguna protección, es bastante simple utilizar un formulario de email para enviar spam. Entonces la cuestión es, ¿cómo podemos protegernos de esto? Existen varias formas de hacerlo, y cada una de ellas provoca una respuesta distinta en los esfuerzos de los spammers para saltear estas protecciones. De a poco, vamos a ir viendo cuidadosamente cada una de estas técnicas, por lo que te recomiendo que sigas leyendo con atención hasta el final del artículo.

Chequeo de email válido

Como el campo email fue utilizado para enmascarar la lista bcc, y además es bastante inteligente evitar recibir comentarios de gente que no se molesta en dejar su email, una forma de prevenir ambas cosas es testear si el email es válido. Esto no es suficiente para parar el ataque, pero es un paso más cerca del principio básico que dice que no hay que confiar en ningún valor editable por el usuario.

Para comprobar que el email esté bien formado, podemos usar expresiones regulares:

mail.php (2)


<?php
$para = "pepe@dominio.com";
$asunto = "Email del website";
$mensaje = $_REQUEST["mensaje"];
$email = $_REQUEST["email"];

if ( ! preg_match('#^[a-z0-9.!\#$%&\'*+-/=?^_`{|}~]+@([0-9.]+|([^\s]+\.+[a-z]{2,6}))$#si', $email) )
{
     echo "Email inválido";
     exit();
}

$headers = "From: $email";
mail($para, $asunto, $mensaje, $headers);
echo "Gracias por su mensaje.";
?>

Es muy inocente conformarse con eso. Cambiemos el ejemplo solo un poco agregando el campo asunto en el formulario:

form.html (2)


<form action="mail.php" method="post">
   Email: <input type="text" name="email" value="">
   Asunto: <input type="text" name="asunto" value="">

   Mensaje: <textarea name="mensaje"></textarea>
   <input type="submit" value="Envie su mensaje">
</form>

mail.php (3)


<?php
$para = "pepe@dominio.com";
$asunto = $_REQUEST['asunto'];
$mensaje = $_REQUEST['mensaje'];
$email = $_REQUEST['email'];

if ( ! preg_match('#^[a-z0-9.!\#$%&\'*+-/=?^_`{|}~]+@([0-9.]+|([^\s]+\.+[a-z]{2,6}))$#si', $email) )
{
     echo "Email inválido";
     exit();
}

$headers = "From: $email";
mail($para, $asunto, $mensaje, $headers);
echo "Gracias por su mensaje.";
?>

Una vez más, se puede tomar el enfoque anterior para abusar de este nuevo formulario con algo como esto:

http://tu-dominio.com/mail.php?mensaje=gotcha&email=hola@dominio-falso.com&subject=VIAGRA%0Abcc:spam-1@undominio.com,spam2@un-dominio.com

Como evitar todo este tipo de spam

El campo bcc no es el único que debería preocuparnos. Hay otras cadenas peligrosas que los spammers pueden usar tales como content-type, mime-version, multipart/mixed, cc. No voy a listar acá todos los posibles exploits que existen, sino que quiero que se note que todos ellos tienen algo en común.

Lo que los hace similares es que los spammers inyectan información en campos que se les permite modificar. Para evitar todo tipo de ataque hay que tener una perspectiva pesimista. Es decir, considerar que todo dato ingresado es potencialmente dañino hasta que se demuestre lo contrario.

Por ejemplo, se puede hacer un nuevo cambio en el script para busca la existencia de ciertas cadenas sospechosas en los valores enviados. Si están, el mail no se envía. De forma similar, la existencia de caracteres de nuevas líneas (%0A) generalmente es un indicador de que algo anda mal.

Una vez más, se pueden utilizar expresiones regulares para buscar este tipo de cadenas y evitar enviar el correo si encontramos algo sospechoso.

Algunas técnicas más específicas

Existen más formas de parar el spam, sin embargo, todo lo que hagamos no es suficiente. El enfoque que existe hoy en día es hacer todo lo que esté a nuestro alcance para complicar la tarea del spammer hasta que finalmente se frustre o se pregunte si realmente vale la pena utilizar tu formulario para enviar spam o si le conviene buscar otro con menos seguridad.

Por ejemplo, varias de estas técnicas consisten en asegurarnos que la información viene de dónde queremos y en la forma que queremos. Si esperamos que venga desde formulario.html, podemos ver el referrer que nos indica el navegador. Así mismo, si los datos se esperan por el método POST, también se pueden comprobar que lleguen de esa forma. Es bueno chequear esto desde el servidor en sí mismo y no con PHP.

Otra buena técnica es almacenar los IPs que solicitan las distintas páginas de nuestro sitio. De esta forma podemos asegurarnos que antes de que nos lleguen datos a procesar pedidos por cierta IP (por ejemplo mandar un email), ésta haya pasado antes por la página del formulario correspondiente. Esto evita el problema del referrer falso que envía directamente datos al script encargado de procesar los emails.

Conclusión

Existen muchas otras cosas que se pueden hacer para mejorar el script. Podés armar un log de los intentos de spam y guardarlos en una base de datos o un archivo. Existe información que es útil para aprender cómo se pueden abusar de tu formulario de emails. Por ejemplo, las IPs, referrer, navegador (user-agent), datos enviados, forma del envío, etc.

En definitiva, el spammer va a seguir intentando abusar de tu script si obtiene un gran beneficio a largo plazo, sin importar el esfuerzo. Implementá estas cositas que te recomiendo y andá chequeando periódicamente tus logs para detectar intentos de spam. La seguridad necesita constante atención.

Comentarios

Excelente articulo... es justo lo que estaba buscando en Google, muchas gracias.

Intentare implementar tus recomendaciones en mis sitios web.

Gracias!

Escrito por Abel | 2007.05.15 17:27:46

Muy Bueno el post!

Escrito por Juan | 2007.09.19 19:53:14

Es muy útil este procedimiento y muy sencillo a comparación de otros, solo quisiera saber como mandar mas cajas de texto es decir que en el mail aparezcan n campos: nombre, telefono, dirección etc.

Escrito por Hugo | 2007.11.01 17:39:58

Lo único que tenés que hacer es, siguiendo el ejemplo, concatenar esos campos que vos decís en la variable $mensaje. Es decir, en el cuerpo del mensaje.
Como la funcion mail() envía mails en formato HTML, le podés dar el estilo que vos quieras.

Escrito por Bruno | 2007.11.05 10:48:28

yo cree los dos archivos , el form y el email, y cuando mando lo pruebo, me dice el siguiente error

Warning: mail() [function.mail]: Failed to connect to mailserver at "localhost" port 25, verify your "SMTP" and "smtp_port" setting in php.ini or use ini_set() in D:\a\mail.php on line 8 Gracias por su mensaje.

y no manda nada, no se que estoy haciendo mal.
en $para pongo un correo de hotmail y en el campo email pongo un correo de yahoo
Espero una respuesta, saludos atte.

Escrito por ariel | 2008.01.12 12:11:29

No hiciste nada mal. El problema está en que no se puede conectar al SMTP como dice la descripción. Fijate cómo está armada tu configuración que el problema reside ahí.

Escrito por Bruno | 2008.01.14 22:18:53

Excelente post!!!

Tengo una necesidad, a ver si me pueden ayudar.

Yo tengo un form, en mi sitio, el cual, ya lo he probado y funciona correctamente. El asunto es el siguiente:
Yo utilizo el servicio de gmail, para tener cuentas coorporativas. Por lo tanto tengo mi info@eldominio.com.ar con una maravillosa interfase Gmail. En fin, esto me trae acarreado un problema, que en el hosting de eldominio.com.ar, la cuenta info no existe. Por lo tanto cuando el php intenta enviar el mail desde y para info@eldiminio.com.ar me dice que el smtp no es es correcto (logicamente)
Ahora la pregunta es, desde donde se me ocurre poder solucionarlo...
¿puedo hacer que el form, utilice el smtp del hosting (es decir utilice la cuenta para enviar con un nombre cualquiera luego de crearla) y hacerlo llegar a info@ (cuenta con interfase Gmail)

espero haber sido claro para que alguien me pueda ayudar. gracias

Escrito por Lucas Sollazzo | 2008.04.11 16:08:39

Claro, para la salida podés usar cualquier smtp al que tengas acceso. De hecho el campo From ni siquiera es necesario.

Escrito por Bruno | 2008.04.15 19:47:52

Comentarios cerrados