La Página de DriverOp

Formularios de contacto en páginas webs.

Sugerencias, trucos y consejos para combatir spambots en formularios de contacto con tips en código PHP y JavaScript. Implementación de CAPTCHA o Código de Confirmación Visual.

Contenido

Introducción.

Desde que mantengo este sitio web he aprendido muchas cosas interesantes. Principalmente el lenguaje PHP del cual no estoy arrepentido y aliento al lector a que lo haga. Pero también he aprendido algunos "gajes del oficio" de un webmaster. En este artículo les comentaré sobre un tema específico: formularios de contacto y la lucha contra el SPAM.

Para clarificar esto comenzaré definiendo algunos términos y usos según los entiendo yo (esto quiere decir que no necesariamente se corresponden con el significado técnico de éstos términos). Un formulario de contacto es un formulario web (creado con el tag "form") que sirve para que el visitante de la página web pueda enviar un mensaje al o los webmasters o a cualquier persona que lo requiera. Esto puede ser usado como un mecanismo para generar un mensaje de correo electrónico o como forma de postear un mensaje en un blog o libro de visitas. No importa cuál es el resultado final del formulario de contacto mientras este sirva como medio para hacer llegar un mensaje escrito a la persona interesada.

Es importante aclarar que todo formulario de contacto supone la utilización de recursos del servidor donde está alojado el formulario, por parte del usuario del mismo. Es decir, cada vez que un usuario usa un formulario dispara un proceso en el servidor; el servidor hace algo. Esto lleva implícito que un formulario es una fuente potencial de problemas, tanto para el dueño del sitio web como para el servidor en el cual está hosteado el sitio en su totalidad.

Por lo tanto el webmaster (o webmistress) debe dedicar tiempo y esfuerzo en no solo proporcionar el servicio en cuestión sino además asegurar que su sitio, el servidor y los recursos implicados estén a salvo de abusos por parte de los usuarios. Los recursos implicados pueden ir más allá que solo el servidor que proporciona el hosting sino además, cuando se usan formularios de contacto que generan correos electrónicos, deben asegurarse el o los servidores de correo y las cuentas de destino de los mensajes generados desde el formulario.

Estos abusos pueden ser de varios tipos:

  • Flood: envio masivo, a veces repetitivo, de mensajes usando el formulario.
  • Tampering: se trata de enviar uno o más mensajes de gran tamaño que tienen la intención de taponar la cuenta de correo, o llenar de basura el espacio asignado por el hosting (agotar la cuota de espacio en disco).
  • SPAM: sin duda es el abuso que todo webmaster más odia (excepto si el propio webmaster es un spammer). Hay dos clases de spam que se puede generar através de un formulario web. Estos son:
  • Dirigidos hacia el propio sitio. Especialmente cierto cuando se trata de un formulario usado en un blog o libro de visitas pues la intención del spammer es aprovechar la visibilidad pública del mensaje para cumplir su propósito.
  • Dirigido hacia otras cuentas de correo. En este caso se aprovecha el formulario para usarlo de pasarela, generalmente explotando alguna vulnerabilidad en el script que permite que el mensaje sea enviado hacia una cuenta diferente de la que el webmaster ha elegido como destino de los mensajes desde el formulario en cuestión.

En este artículo hablaré de técnicas para evitar el SPAM generado desde un formulario de contacto, despreciando los otros tipos de abusos aunque el lector podrá aprovechar lo aquí comentado para inventar mecanismos para evitar esos abusos también. Pero antes de comenzar debo enunciar la regla de oro cuando se trata de combatir spam desde páginas webs: nunca jamás y por ninguna razón publiques la dirección de correo electrónico de destino de un formulario web. Al menos no en texto en claro. Usar "mailto:" es una invitación a que te envien spam. Y aunque un campo oculto no se ve desde un navegador eso no quiere decir que nadie puede verla.

Mecanismos de envio.

Técnica de un solo paso.

Llamo así al mecanismo por el cual la página o script se llama así mismo cada vez que el usuario da la orden de envio desde el formulario.

Técnicamente, el navegador hace un GET de la página o script, el script detecta esto y genera el formulario para que el usuario lo rellene. El formulario en su parámetro "action" apunta a la propia página. Cuando el usuario termina de rellenar los datos solicitados el navegador hace un POST hacia la propia página, el script detecta esto y analiza el contenido actuando en consecuencia.

La ventaja (desde el punto de vista del programador) de esta técnica es bastante obvia, hay un solo archivo para mantener.

Las desventajas las veremos más adelante.

Técnica de dos pasos.

A diferencia de la anterior el proceso está dividido en dos scripts o páginas. Una de ellas presenta el formulario que apunta al segundo script que es el que procesa la petición. El navegador hace un GET de la primera página y luego un POST hacia la segunda.

Al tener dividio el proceso en dos partes otorga cierta flexibilidad, por ejemplo, el segundo script puede ser genérico en el caso de que tengamos más de un formulario (este sería el caso de un sitio que ofrece distintos formularios de contacto dependiendo del tipo de servicio que ofrece: soporte técnico, ventas, etc...). Con la técnica anterior esta flexibilidad se pierde.

El segundo script es "ciego", esto es, no genera código HTML hacia el usuario, se limita a procesar los datos del formulario, generar el correo electrónico (o entrada en el foro/libro de visitas) y devolverle el control al primer script para que éste informe de los resultados del proceso.

Técnica de tres pasos.

Aquí tenemos tres páginas o scripts. La primera presenta el formulario (GET), envia los datos recogidos al segundo (POST) que se encarga de formatearlos apropiadamente y luego llama al tercero quien es el que efectivamente enviará el mensaje. La ventaja es lograr centralizar el envio de mensajes en un solo lugar, mientras que el formateo de datos puede variar según las necesidades del sitio. La otra ventaja desde el punto de vista de la seguridad es que el visitante no puede saber de la existencia de ese tercer script que es el que realmente envia el mensaje, ya que su nombre nunca se hace visible.

Validación de campos en el cliente.

En informática hay un viejo refrán que dice: "basura que entra es basura que sale" (GIGO, garbage in, garbage out); esto quiere decir que si no se controla la entrada de datos cualquier cosa puede salir luego de procesarlos. Por lo tanto la primera línea de defensa contra la basura es limitar y obligar al usuario a que ingrese lo que se le pide, en el lugar que se le pide y que esos datos tengan un mínimo de sentido.

El lenguaje de marcación HTML provee mecanismos para validar datos aunque son en extremo limitados, por ejemplo tenemos tags que generan campos de una sola línea, de más de una línea, radiobuttons, checkboxes, etc..., para los campos de texto apenas es posible limitar el tamaño de la entrada. Este tipo de validaciones se llaman estáticas porque no es necesario escribir código específico para que esta validación ocurra pero esto no quiere decir que sea inútil. Por el contrario, ya que están ahí úsalos.

Para propósito de prevención de spam es una buena idea no dejar sin límite los campos de textos de una sola línea (especificar el parámetro "maxlength" en el tag "input"), lamentablemente el tag "textarea" no tiene un "maxlength", por lo que habrá que recurrir al uso de JavaScript para controlar el máximo de caracteres permitidos.

El uso de JavaScript es la segunda línea de defensa. Debes armarte de paciencia y tratar de aprender a programar en este lenguaje y poner cuanto código sea necesario para controlar la validación de datos.

Sin embargo mi experiencia me dice que la cosa no termina aquí. La cuestión es muy sencilla. Tanto HTML como JavaScript funcionan bien mientras el que está visualizando el formulario sea un usuario legítimo, con un navegador web legítimo, pero nada de esto funciona frente a un spambot porque el bot ignorará todo lo que le parezca supérfluo y definitivamente ningún bot ejecutará JavaScript (al menos no he visto ninguno que lo haga), de lo contrario sería muy fácil causar un fallo en el bot (el cazador cazado!) escribiendo un pedazo de código en JavaScript que, por ejemplo, dispare un ciclo infinito hará que el bot se cuelgue. Teniendo esto en cuenta deberíamos programar un formulario que contemple tanto la funcionalidad "legítima" del mismo y también ofuscar tanto como sea posible la forma en que un bot "ve" el formulario.

Hay un dato que se debe usar con precaución. Los tags HTML que sirven para diseñar formularios incluyen un par de parámetros, "id" y "name", por tonto que parezca y aunque esto el usuario legítimo no lo vea (ni tiene por qué verlo) los bot sí lo ven y lo tienen en cuenta. Para comprender el peligro que encierran estos parámetros debemos saber cómo funciona un formulario web.

Una vez que se hace click en "Enviar" (por ejemplo) el navegador debe armar la petición POST poniendo los datos que el usuario ha ingresado, para ello se vale de esos parámetros antes mencionados. El navegador los ordena de acuerdo al orden en que aparecen en el código HTML cada uno seguido con el dato asociado. Los bots deben hacer lo mismo. Sin embargo no todos los formularios son iguales aunque la mayoría sigue un patrón. Si nuestro formulario es uno diseñado para enviar un correo generalmente está compuesto de al menos dos campos: uno para el correo del remitente, otro para el cuerpo del mensaje. Eventualmente se podrían usar otros más, pero esos dos son lo mínimo y necesario, y esto los bots lo saben. Claro que hay muchos tipos de formularios que también podrían tener dos campos como estos, la diferencia está en que los programadores suelen (solemos) nombrar a esos campos como "email" y "mensaje", o "correo" y "msg", o "direccion" y "body", y esto los bots también lo saben.

Esto es especialmente cierto ahora que son tan populares los blogs hechos con scripts prefabricados como WordPress o Blogspot. Hay bots que ya saben cómo están ordenados los campos de los formularios que estos scripts generan, y los aprovechan. El consejo aquí es no ponerle nombres obvios a los campos de los formularios. Mejor ponerle nombres aleatorios y sin sentido. Y lo ideal es que el formulario mismo sea generado con JavaScript aunque como veremos más adelante esto no será necesario.

Pero, ¿qué tan grave es eso?, no mucho en realidad. Hay spambots que no tienen en cuenta nada de esto y envian la petición POST con cualquiera sea el nombre que le hayamos puesto a los campos, aunque me ha pasado casi desde la primera vez que hice un formulario web, ví que hay bots que sí lo tienen en cuenta. Me pareció extraño que algunas peticiones de bots sabían dónde poner los datos y en qué orden. Cambié el orden de los campos y aún así sabían cómo formar la petición correctamente. Solo cuando modifiqué los nombres y los ids de los campos esos bots dejaron de funcionar.

Esto de los bots es un problema serio, muy serio, tal como cualquier blogger les puede decir. Los hay muy simples y los hay muy elaborados. Los primeros solo se limitan a extraer el nombre del script del parámetro "action" del tag "form" y enviar cualquier cosa en el POST hacia ese script y los hay que hacen un análisis completo del formulario, tal como adivinar en base al nombre ("name") y id de cada campo qué dato debe ir allí.

Validación de campos en el script.

Otro refrán, no muy conocido, dentro de la informática que es, digamos, casi una regla de facto dice: "No confiar nunca en el usuario"(y no soy el único que opina esto). Creo que se trata de una reformulación de la Ley de Murphy: "Si algo puede fallar, tarde o temprano fallará". O, como escuché una vez: "Puedes hacer un programa a prueba de tontos pero no puedes hacer un programa a prueba de un maldito tonto!".

El problema es que si dejas que la validación de datos la haga solamente el cliente estás dejando la responsabilidad en algo que está fuera de tu control. Una vez que el navegador cargó la página ya no sabes qué está ocurriendo con ella, solamente cuando éste te envia el POST puedes ver qué ha pasado. Aquí debes ser estricto.

No confies en el parámetro "maxlength", vuelve a medir el tamaño de los campos. No confies en esa función JavaScript que te valida que una dirección de e-mail esté bien formada. Vuelve a verificarla. También verifica que todos los campos obligatorios no están vacíos. Y también verifica que los campos tipo "select" estén dentro de los rangos esperables. Que las fechas sean fechas válidas, etc... En fin, verifícalo todo como si no existiera ninguna otra validación previa. Esto ayuda mucho a combatir los spambots (y usuarios tontos :P).

Y también verifica que en la petición POST existan todos los campos que has puesto en el formulario, incluyendo los "hidden". O sea, usando PHP verifica que exista el array $_REQUEST y que cada posición del array se corresponde con los campos del formulario.

A la primera señal de que algo está mal aborta el script e infórmale al usuario de lo sucedido.

Discriminar máquina-humano (Captcha o Código de Confirmación Visual).

Así como hay gente que se dedica a programar bots spamers hay gente que se dedica a tratar de burlarlos. La forma más efectiva y más extendida de hacer esto se llama "Captcha" o Completely Automated Public Turing test to tell Computers and Humans Apart (Prueba de Turing pública y automática para diferenciar a máquinas y humanos), también llamado Código de Confirmación Visual. A pesar del impresionante nombre de esta técnica es algo muy sencillo de implementar, de hecho estoy casi seguro que tú ya has visto esto al menos como usuario: se trata de escribir en una caja de texto las letras, números o palabras que la página te muestra en un pequeño gráfico o imagen. Esta imagen generalmente tiene ruido de fondo, rayas, dibujos aleatorios, puntos, cambios de colores, las letras suelen estar distorsionadas, etc. Esta técnica es muy efectiva y se basa en la siguiente asunción: es muy, muy, muy difícil hacer un bot que sea capaz de interpretar correctamente el contenido de una imagen. Además el bot que intente hacerlo consumirá muchos recursos. Básicamente para poder interpretar o extraer la parte útil de la imagen es necesario programar un reconocedor de caracteres (OCR) con todo lo que ello implica. Quienes hemos usado alguna vez un programa OCR sabemos que tienen muchos fallos, son lentos y consumen tiempo de procesador, todas estas cosas son las que tratan de evitar quienes ponen un bot para rellenar formularios con el objeto de hacer spam.

Mientras que para nosotros, quienes programamos el formulario, es muy sencillo implementar esta medida de seguridad. Contamos con la habilidad del ser humano en reconocer fácilmente con solo ver la imagen qué es lo que hay en ella.

Llevado a la práctica si tu servidor puede ejecutar PHP y cuenta con la biblioteca GD puedes encontrar scripts o snippets en Internet que ya tienen implementada la técnica Captcha.

¿No tienes la biblioteca GD en tu servidor o no puedes instalarla?, aún estás a salvo. No es necesario recurrir a imágenes. Basta con pedirle al usuario que responda una simple pregunta al azar que solo un ser humano puede responder. Por ejemplo preguntas del tipo "¿De qué color es el cielo?", "¿Cuánto es dos mas dos?", "Lo contrario de hombre es...?", "Si algo no es verdadero es...?" o preguntas por el estilo. El resto es nada más que comparar la respuesta del usuario con la respuesta correcta. Sencillo ¿no?.

Restricción de referer.

En mi caso he llegado a la conclusión de que la mejor forma de programar un formulario web es hacerlo con la técnica de los tres pasos visto más arriba.

Se hace una página HTML con el formulario; a esta página y a modo de ejemplo la llamaré "formulario.htm".

Se hace un script en PHP que será el receptor de ese formulario y que se encargará de validar del lado del servidor los datos del formulario, a este le llamaré "validador.php".

Y finalmente un segundo script que se encargará de gestionar la conexión al servidor SMTP para enviar el correo (o bien será el encargado de agregar el registro a la base de datos en el caso de que se trate de un foro o libro de visitas o comentario de un blog), a éste le llamaré "enviador.php".

Formulario.htm "sabe" de la existencia de validador.php pero no de la existencia de "enviador.php" ni tiene por qué saberlo. Esto es equivalente a decir que sea lo que sea que esté cargando formulario.htm tampoco sabe de la exitencia de enviador.php.

Podemos asumir que validador.php funciona como un cortafuego (firewall) que aisla a enviador.php del "mundo exterior".

1.- El navegador carga el formulario (petición GET).

2.- El navegador envia los datos (POST) al script validador.php, este procesa los datos.

3.- Si todo está bien, validador.php invoca a enviador quien será quien envie el mensaje.

4.- Enviador.php devuelve el resultado del envio.

5.- Validador.php genera una página de resultados.

6.- El navegador carga la página de resultados (petición GET).

He puesto un asterico en resultados.htm porque esta página puede ser la misma que formulario.htm. En el caso que haya un error con los datos (paso 2), validador no llamará a enviador sino que forzará al navegador a recargar formulario.htm para que el usuario vuelva a cargar los datos correctamente (esto se hace con la función header() de PHP).

He mostrado este gráfico para poder introducir otro mecanismo de seguridad que es la restricción de referer. Cuando un navegador solicita una página al servidor en la misma petición le indica al servidor desde dónde ha obtenido el nombre de la página que quiere que el servidor le sirva. A ese dato se le llama "referer" (en PHP hay una variable superglobal del servidor para saber este dato: $_SERVER["HTTP_REFERER"]) que incluye la URI completa, es decir, el dominio más el path con el nombre de la página donde el usuario hizo clic; o lo que es lo mismo, el lugar donde está el hiperlink hacia la página que quiere cargar. Normalmente este dato es útil para saber desde dónde han linkeado nuestra página pero para el propósito de este artículo este dato nos servirá para algo más.

Observando el gráfico se puede ver claramente que validador.php puede saber cuál es su referer (formulario.htm), pero como he mencionado antes un bot puede determinar qué script procesará el formulario solamente viendo el parámetro "action" del tag "form". Hay bots que aprenden esto y se saltean la petición a formulario.htm y directamente invocan mediante un POST al script validador.php, si eso ocurre el referer de validador.php quedará vacío. Habiendo explicado esto es facil comprender que validador.php también debe comprobar que el referer exista y que éste sea formulario.htm (o cualquier otro que nosotros queramos), caso contrario abortar el script.

Realmente esto es cierto. Tengo en los logs de mis sitios webs casos como el ya comentado. Llamadas al script validador.php que no provienen de ninguna parte.

Para implementar esto basta con crear una lista de los dominios admitidos como referer del script y comparar el referer enviado por el navegador (o bot) contra esa lista.

Más aún. Formulario.htm también debería poseer este mecanismo. Si queremos que el usuario pase por cierto lugar antes de llegar al formulario, por ejemplo, la página principal de nuestro sitio, sería conveniente implementar esta validación antes de mostrar el formulario. Claro que corremos el riesgo de confundir al usuario porque si ponemos un link que dice "Contacto" y el usuario decide copiar el link y abrirlo en una página aparte, no habrá referer. Desde el punto de vista de nuestra página un usuario legítimo en esta situación sería indistinguible de un bot. Pero hay una solución: identificar el navegador.

He visto que algunos bots no informan al servidor el "user-agent". El "user-agent" es análogo al "referer" ya mencionado y aunque no es obligatorio según el protocolo HTTP, todos los navegadores serios lo proporcionan como parte de una petición GET o POST. Por lo tanto es una buena medida verificar de la existencia del mismo antes de mostrar el formulario. Y también debería ser una medida de seguridad más en el script validador.php.

La variable superglobal de PHP que contiene este dato es $_SERVER["HTTP_USER_AGENT"].

Por supuesto, un bot competente, que los hay, tienen en cuenta estos dos detalles. Especialmente los bots de "doble pasada" (el término me lo inventé yo); se trata de bots que primero hacen una petición GET de la página con el propósito de ver si ésta contiene un formulario, si lo detecta memoriza la página, vuelve a cargar la página, y compara lo recibido con lo anterior para ver las diferencias. Hay bots que se dan cuenta que estás validando tanto referer como user-agent y cuando comienzan el ataque rellenan esos campos apropiadamente y lo hacen tan bien que son indistinguibles de una petición legítima.

Restricción de IPs.

Éste mecanismo debería ser un recurso extremo. No lo aconsejo excepto que uno sepa fehacientemente que hay un bot en esa IP en particular (o en un rango de IPs). Hoy en día los bots no trabajan desde una única conexión. La gran mayoría de bots conforman "botnets", son redes de bots que se distribuyen en forma de virus, el bot se ejecuta sin que el dueño de la computadora lo advierta y el bot recibe instrucciones remotamente desde el dueño de la "botnet".

Podemos darnos cuenta de este comportamiento mirando los logs de accesos a nuestro sitio. Vemos que una IP hace una petición GET a la página donde está el formulario e inmediatamente después con diferencia de dos o tres segundos otra IP totalmente distinta a la enterior hace un POST al script validador.php. Estas "duplas" se repiten cada tanto, siempre las parejas son distintas. A estos bots les llamo de "doble pasada".

En PHP puedes obtener la IP del visitante mediante la variable superglobal del servidor $_SERVER["REMOTE_ADDR"];

Trampas antibots.

Anteriormente había mencionado el Captcha usando una pregunta o una imagen que solo puede ser interpretada por un ser humano. Pero también es posible implementar un mecanismo que solo sea interpretado por una máquina pero no por un ser humano.

Lo admito, lo que a continuación les contaré lo descubrí por accidente. Estaba diseñando una página en donde debía colocar una capa (tag "div") en posición absoluta e inadvertidamente tecleé una posición que estaba fuera de la pantalla, la posición era top: -100px. Cuando probé la página ¡la capa había desaparecido!. Bueno, no, no había desaparecido, simplemente estaba fuera de la pantalla, no podía verla pero estaba allí.

El campo "invisible".

Entonces se me ocurrió la siguiente idea: poner un input, es decir un campo de formulario de una línea, vacío dentro de un div en posición absoluta fuera de la pantalla, digamos con top: -1000px. y left: -1000px. como parte de un formulario de contacto. Y luego en validador.php comprobar que ese campo esté vacío. Un usuario (un ser humano usando un navegador legítimo) no puede ver ese campo, tampoco rellenarlo, pero un bot sí puede verlo y sabiendo que los spambots aprovechan para rellenar cuando campo no oculto haya en el formulario sí lo hará. Creo que no necesito comentar nada más. Esta es una buena trampa antibots.

El campo "no lo toques".

Rizando el rizo se me ocurrió otro mecanismo más. Poner un campo input con un valor por omisión e informarle al usuario, preferentemente con un texto dentro de una imagen estática, que no modifique el valor de ese campo. Un ser humano hará caso de ello, un bot probablemente no lo hará.

No poner nombres obvios.

Con el auge explosivo de los blogs y la aparición de aplicaciones webs hechas con lenguajes de scripts tal como PHP, para hacer páginas webs completas (los llamados CRS o Content Management System), han aparecido spambots especializados en atacar a esas aplicaciones basándose en la premisa de que esas aplicaciones tienen nombres fijos y conocidos para los scripts validadores o enviadores así como también los ya mencionados "names" y "ids" de los campos de los formularios "estandar" que esas aplicaciones generan.

La primera vez que hice un formulario de contacto pensé en implementar la técnica de los tres pasos antes descripta pero sin tener en cuenta a los spambots (vamos, error de principiante), tuve la mala idea de nombrar al archivo que validaba los campos del lado del servidor de la misma forma que lo hacía un famoso CRS. ¿El resultado?, de pronto comencé a ver IPs en el log de acceso que hacían POST a ese script como si supieran qué es lo que hacía. Esto me dejó desconcertado (y es una de las razones que me impulsó a investigar este tema). La moraleja: no pongas nombres obvios. En mi caso fue "send.php", ese nombre debes evitarlo. Tampoco uses cosas como "blog", "mail", "email", "correo", "sender", "msg", "preview", "swift", "proc", "module" o "admin".

Ocultación de archivos.

Finalmente tenemos el último problema (por el momento): el parámetro "action" del tag "form" revela al spambot el nombre del script validador. Sería una buena idea que el nombre de ese archivo estuviera oculto, pero nos surge el problema de que cómo determinar que quien está pidiendo el formulario es un bot o un usuario legítimo.

Se me ocurren dos formas de abordar el problema.

La primera es utilizar la asunción de que un spambot muy probablemente no ejecute JavaScript, en detrimento de aquellos usuarios legítimos que tienen JavaScript desactivado en sus navegadores, pero eso se puede solucionar informando explícitamente que al menos esa página necesita de JavaScript.

JavaScript tiene un mecanismo que nos permite hacer esto, se trata de la propiedad action del objeto forms[] del objeto document, o: document.forms[0].action="validador.php". Una función típica sería esta:

  function fakeaction() {
    document.forms[0].action="validador.php";
    document.forms[0].submit();
  }

E invocándola en HTML como:

  <input onclick="fakeaction()" type="button" name="enviar" value="Enviar">

dejando vacío el parámetro "action" del tag "form".

Para la segunda solución vamos a ponernos paranóicos. Que el spambot no ejecute JavaScript no quiere decir que no sea capaz de entenderlo. Ese "submit()" en el código se ve muy sospechoso. Así como nosotros ideamos esta "trampa", los programadores de un spambot también pueden descubrir el truco (y no dudo de que ya lo hayan hecho). Desde el punto de vista de ellos solo hace falta ver que si el parámetro "action" del tag "form" está vacío y no hay un input con type="submit" en el formulario y hay un submit() en el código JavaScript solo hace falta ver si la propiedad action del objeto forms del objeto document tiene asignado un valor. ¿Estoy yendo demasiado lejos?, pues no, nunca hay que subestimar el ingenio humano. Vamos a ponerle un obstáculo más al spambot.

Ese "validador.php" sigue estando allí, al menos en texto trivialmente legible. Pero qué tal si lo ofuscamos de alguna forma que no sea trivial leerlo pero aún pueda ser interpretado correctamente por un navegador legítimo. Recurriremos al UNICODE.

Es posible escribir texto en UNICODE que todo navegador serio es capas de interpretar. En la práctica la cadena "validador.php" traducido de ASCII a UNICODE se vería así:

\u0076\u0061\u006c\u0069\u0064\u0061\u0064\u006f\u0072\u002e\u0070\u0068\u0070

Y eso es exactamente lo que vamos a hacer:

    function fakeaction() {
    document.forms[0].action="\u0076\u0061\u006c\u0069\u0064\u0061
\u0064\u006f\u0072\u002e\u0070\u0068\u0070";
    document.forms[0].submit();
  }

No hay que crear falsas expectativas. Esto no es la panacea. Peor que la falta de seguridad es la falsa sensación de seguridad pero convengamos que les hemos puesto las cosas bastante difíciles, ¿cierto?.

¿Rizamos el rizo de nuevo?. Sí, cómo no. Bien pues, veo un pequeño detalle que se puede trabajar. El nombre del script validador sigue siendo fijo, ofuscado mediante UNICODE y todo sigue siendo un nombre predecible. Lo menciono por lo siguiente: he visto que una vez que un bot descubre cuál es el script que hace el trabajo ya no necesita pasar por el formulario, como vimos antes, habíamos puesto la verificación mediante referer para asegurarnos que el script fue realmente invocado desde el formulario, algo que, como ya comenté, se puede simular. ¿Pero qué tal si el script validador es de un solo uso?.

Con esto quiero decir que tendríamos que idear una manera en que el script validador solo pueda ser usado una vez, luego de su uso descartarlo. Pero ¿cómo?.

Pues es obvio que si somos capaces de ejecutar script en el servidor y tenemos acceso de lectura y escritura en nuestro directorio nada nos impide copiar un archivo, ¿cierto?. Aprovechando esto podríamos poner el script validador en algún lugar de nuestro directorio home, asignándole un nombre nada obvio, por ejemplo "rodadilav.php". Luego agregar a la página del formulario un script PHP que copie ese archivo a uno nuevo con un nombre generado aleatoriamente. Ese nombre convertirlo a UNICODE (aquí está la tabla de conversión) y reescribir la porción de JavaScript con ese nuevo nombre.

En PHP sería algo así:

  $scriptvalidador = GetNombreAleatorio();
  copy("rodadilav.php",$scriptvalidador);

$nombreunicode = ConvertASCIItoUNICODE($scriptvalidador);

Donde GetNombreAleatorio() y ConvertASCIItoUNICODE() son funciones en PHP escritas por nosotros en alguna parte (se los dejo como ejercicio al lector).

Parece sencillo visto así pero hay algunos problemas con esto. No todos los usuarios terminarán por dar clic para procesar el formulario y ese copy() hará que con el tiempo se nos llene el directorio de archivos que son copias del script validador, además no estamos logrando lo que queremos en un principio el cual es que cada copia del script validador solo pueda ser usado una sola vez.

Entonces conviene eliminar cuanto archivo de copia del script validador haya en el directorio. Esto lo podemos lograr con PHP también pero teniendo cuidado de borrar solo y únicamente aquellos archivos que nos interesan. Debemos tener una forma de identificar esos archivos. Una manera de hacerlo es poniendo como parte del nombre del archivo copiado una parte fija y otra variable (esta última es la generada aleatoriamente) digamos así: _xxxxxx_.php donde las "x" serán números y letras aleatorias y los guiones bajos la parte fija.

Para eliminar solo y únicamente aquellos archivos que cumplen el patrón descripto en el párrafo precedente procedemos en PHP así:

  $dir = opendir("."); // se crea un handle del directorio actual
  while (($archivo = readdir($dir)) !== false) { // mientras haya archivos...
  // si el 1er y 7mo caracter en el nombre del archivo es guión bajo
  // y la extensión es php
     if (($archivo[0] == "_")
     and ($archivo[7] == "_")
     and (getFileExtension($archivo) == "php"))
     { unlink($archivo); } // se borra el archivo
  } // while
  closedir($dir); // se cierra el handle

Y listo.

El avispado lector se habrá dado cuenta de un detalle. Este código no es reentrante. Vuelvo atrás. Nuestro objetivo hasta acá está cumplido, cada vez que la página se carga se eliminan todas las posibles copias del script validador y se genera una nueva copia de forma tal que si es un spambot el que cargó la página como mínimo solo puede usar el script una sola vez ya que la próxima vez que intente saltearse la página del formulario el archivo que el spambot cree que es el destino del formulario muy probablemente ya no esté allí (obtendrá un status HTTP 404), eliminado por una petición siguiente a la suya.

Pero en el mundo real las cosas no son tan fáciles. Puede ocurrir que haya dos peticiones al formulario con pocos segundos de diferencia y puede que esas peticiones sean legítimas, de usuarios legítimos. Si ese es el caso sabemos que a un usuario le lleva tiempo tipear toda la información que el formulario requiere. Cuando haga clic en enviar, el script que se supone procesará esos datos ¡ya no estará allí!.

Una forma de solucionar esto es haciendo uso de la función estandar de PHP filemtime() que devuelve la marca de tiempo de modificación o creación de un archivo en formato UNIX y hacer un cálculo tal que si el tiempo de creación o modificación es de, por ejemplo, hace menos de 10 minutos, no borre ese archivo. Eso debería ser suficiente para la mayoría de los casos.

Conclusión.

A lo largo de este artículo he volcado mi experiencia en el tema de combatir el spam generado através de formularios web que a su vez puede ser aprovechado para otros usos, por ejemplo evitar la registración automática a servicios en línea. Confio que el lector sepa sacar provecho de los consejos, opiniones y "trucos" que aquí he escrito. También es bienvenido a darme su opinión al respecto haciendo uso de... ejem... éste formulario web :).

Apéndice - Creación de un formulario web.

Consideraciones preliminares.

Debido a que luego de publicar la primera versión de este artículo me ha llegado bastante correo preguntándome cosas sobre formularios webs, he decidido agregar este apéndice para explicar cómo implementar las cosas que expongo aquí.

El objetivo de esta suerte de guía de desarrollo es poner un formulario web de contacto para enviar un correo electrónico, similar al que tengo en este mismo sitio.

Para conseguir esto se necesitan las siguientes cosas antes de comenzar el proyecto.

  • Un servidor web capaz de ejecutar PHP.
  • Un servidor SMTP donde tengamos una cuenta de correo electrónico funcional; los "webmails" no funcionan para eso, debe ser un servidor SMTP verdadero. Casi todas las empresas que ofrecen hosting de páginas webs ofrecen un servidor SMTP junto a dicho hosting.
  • En mi caso usaré la biblioteca GD de PHP. Consulta con tu hosting si esta biblioteca está instalada y puedes hacer uso de ella.

Estas tres cosas son necesarias para desarrollar la solución que aquí expongo, si no cuentas con alguna de ellas, éste tutorial no es para ti.

Teniendo estas cosas, pongamos mano a la obra. Crearemos dos archivos desde cero, además modificaremos un archivo ya existente el cual les indicaré dónde encontrar. No es por pereza, sucede que "nunca pienses la solución a un problema dos veces" y "dado que el problema es muy común seguro que alguien ya pensó la solución" ;). La técnica que implementaremos será la "técnica de los tres pasos" tal como la explico más arriba en este artículo.

Formulario de contacto en HTML y una pizca de PHP.

Para comenzar creamos un archivo nuevo el cual llamaremos "formulario.php". En él diseñaremos el formulario de contacto en HTML, usa el editor de HTML que gustes. A continuación está el código fuente de mi versión realmente simplificada:

formulario.php ver 1.0
<html>
<head>
<title>Formulario de contacto.</title>
</head>
<body>
<form name="form1" method="post" action="">
<input name="esto" type="text" id="esto" size="35" maxlength="100"><br>
<input name="esun" type="text" id="esun" size="35" maxlength="100"><br>
<textarea name="ejemplo" cols="50" rows="10" id="ejemplo"></textarea><br>
<input onclick="fakeaction()" type="button" name="Submit" value="Enviar">
</form>
</body>
</html>

Por supuesto, esto es sumamente sencillo y está incompleto. Pero me sirve para señalar algunas cosas. Tal como explico en mi artículo he usado identificadores no obvios para "name" y "id". En el código se puede apreciar que hay dos inputs, uno para el asunto del mensaje, otro para que el visitante ponga su correo electrónico a donde supuestamente se le puede responder al mensaje; y un campo de texto para que escriba el cuerpo del mensaje, más un botón de envio. Éste boton no es de tipo "submit" sino que he puesto una llamada a una función JavaScript (el código fuente de esta función está más arriba en este artículo) la cual incluiremos más adelante, que captura el evento "onclick". Notar además que el parámetro "action" del "form" está vacío.

Completemos lo que falta: la función JavaScript "fakeaction()". Pero como mencioné más arriba esta función debe ser diferente para cada visitante porque recordemos que esta función es la que le revelará al cliente cuál es el script que recibe los datos de este formulario.

Sin la parte de ofuscación UNICODE la página de formulario con el script sería así:

formulario.php ver 1.1
<html>
 <head>
  <title>Formulario de contacto.</title>
  <script type="text/javascript">
   function fakeaction() {
   document.forms[0].action="validador.php";
   document.forms[0].submit();
   }
  </script>
 </head>
 <body>
   <form name="form1" method="post" action="">
   <input name="esto" type="text" id="esto" size="35" maxlength="100"><br>
   <input name="esun" type="text" id="esun" size="35" maxlength="100"><br>
   <textarea name="ejemplo" cols="50" rows="10" id="ejemplo"></textarea><br>
   <input onclick="fakeaction()" type="button" name="Submit" value="Enviar">
   </form>
 </body>
</html>

Lo cual tiene más sentido, ¿cierto?.

No te preocupes de que no exista aún el script validador.php, lo trataremos luego.

Ahora vamos a ofuscar la cadena que le pasamos a "action" en el script Java. Para eso haremos una función PHP que por amor a la claridad la pondré en el mismo archivo formulario.php (los puristas pueden aprovechar y poner esta función en un archivo aparte e incluir ese archivo en éste mediante require() o require_once()).

Ésta es la nueva versión de formulario.php:

formulario.php ver 1.2
<?php
$UNICODE = Array(
   " " => "\u0020",
   "_" => "\u005f",
   "@" => "\u0040",
   "a" => "\u0061",
   "b" => "\u0062",
   "c" => "\u0063",
   "d" => "\u0064",
   "e" => "\u0065",
   "f" => "\u0066",
   "g" => "\u0067",
   "h" => "\u0068",
   "i" => "\u0069",
   "j" => "\u006a",
   "k" => "\u006b",
   "l" => "\u006c",
   "m" => "\u006d",
   "n" => "\u006e",
   "o" => "\u006f",
   "p" => "\u0070",
   "q" => "\u0071",
   "r" => "\u0072",
   "s" => "\u0073",
   "t" => "\u0074",
   "u" => "\u0075",
   "v" => "\u0076",
   "w" => "\u0077",
   "x" => "\u0078",
   "y" => "\u0079",
   "z" => "\u007a",
   "." => "\u002e"
   );
function convertASCIItoUNICODE($cadena) {
   global $UNICODE;
   $result = "";
   for ($i = 0;$i < strlen($cadena); $i++) {
   $result .= $UNICODE[$cadena[$i]];
   }
   return $result;
   }
?>
 
<html>
 <head>
  <title>Formulario de contacto.</title>
  <script type="text/javascript">
   function fakeaction() {
document.forms[0].action="<?php echo convertASCIItoUNICODE("validador.php"); ?>";
   document.forms[0].submit();
   }
  </script>
 </head>
 <body>
   <form name="form1" method="post" action="">
   <input name="esto" type="text" id="esto" size="35" maxlength="100"><br>
   <input name="esun" type="text" id="esun" size="35" maxlength="100"><br>
   <textarea name="ejemplo" cols="50" rows="10" id="ejemplo"></textarea><br>
   <input onclick="fakeaction()" type="button" name="Submit" value="Enviar">
   </form>
 </body>
</html>

Si cargas formulario.php ahora en el navegador y le pides a éste que te muestre el código fuente verás que lo que está entre comillas luego de "action=" es texto UNICODE. Justo lo que pretendíamos.

Ahora que tenemos una cómoda función que nos convierte cualquier cadena ASCII a su equivalente UNICODE podemos implementar el mecanismo que crea una copia de un solo uso del script validador.php. El código lo he expuesto ya más arriba en este artículo, esta vez agregaré el resto de las funciones que nos hacen falta, tal como la que nos devuelve una cadena de caracteres aleatorias. Siéntete libre de modificar lo que creas apropiado, en mi caso usaré como nombre de archivo aleatorio el formato "_xxxxxx_.php" donde "x" es un caracter en minúscula cualquiera.

Esta es la nueva versión:

formulario.php ver 1.3
<?php
$UNICODE = Array(
   " " => "\u0020",
   "_" => "\u005f",
   "@" => "\u0040",
   "a" => "\u0061",
   "b" => "\u0062",
   "c" => "\u0063",
   "d" => "\u0064",
   "e" => "\u0065",
   "f" => "\u0066",
   "g" => "\u0067",
   "h" => "\u0068",
   "i" => "\u0069",
   "j" => "\u006a",
   "k" => "\u006b",
   "l" => "\u006c",
   "m" => "\u006d",
   "n" => "\u006e",
   "o" => "\u006f",
   "p" => "\u0070",
   "q" => "\u0071",
   "r" => "\u0072",
   "s" => "\u0073",
   "t" => "\u0074",
   "u" => "\u0075",
   "v" => "\u0076",
   "w" => "\u0077",
   "x" => "\u0078",
   "y" => "\u0079",
   "z" => "\u007a",
   "." => "\u002e"
);
   
/* -------- Funciones ---------- */
function convertASCIItoUNICODE($cadena) {
   global $UNICODE;
   $result = "";
   for ($i = 0;$i < strlen($cadena); $i++) {
   $result .= $UNICODE[$cadena[$i]];
   }
   return $result;
} // convertASCIItoUNICODE
   
function RandomLetters($n) {
   $abcd = Array("a","b","c","d","e",
   "f","g","h","i","j","k","l","m",
   "n","o","p","q","r","s","t","u",
   "v","w","x","y","z");
   if ($n < 1) { $n = 1; }
   $text = "";
   for ($i = 1; $i <= $n; $i++) {
   $text = $text . $abcd[rand(0,25)];
   } // for
   return $text;
} // RandomLetters
   
function getFileExtension($str) {
   $i = strrpos($str,".");
   if (!$i) { return ""; }
   $l = strlen($str) - $i;
   $ext = substr($str,$i+1,$l);
   return $ext;
} // getFileExtension
   
/* ----------- Proceso ----------- */
$limite = time() - 600; // tiempo límite 10 minutos
$dir = opendir(".");
while (($archivo = readdir($dir)) !== false) {
   if (($archivo[0] == "_") 
     and ($archivo[7] == "_")
     and (getFileExtension($archivo) == "php")) {
        // toma la fecha y hora de creación del archivo
     $ft = filemtime($archivo);
       // si es menor al tiempo límite lo borra
      if ($ft < $limite) { unlink($archivo); } 
   } // if
} // while
closedir($dir);
// crea un nuevo nombre de archivo.
$FNValidador = "_" . RandomLetters(6) . "_.php";
// copia el validador original
copy("validador.php",$FNValidador);
?>
<html>
 <head>
   <title>Formulario de contacto.</title>
   <script type="text/javascript">
   function fakeaction() {
document.forms[0].action="<?php echo convertASCIItoUNICODE($FNValidador); ?>";
   document.forms[0].submit();
   }
   </script>
 </head>
<body>
 <form name="form1" method="post" action="">
   <input name="esto" type="text" id="esto" size="35" maxlength="100"><br>
   <input name="esun" type="text" id="esun" size="35" maxlength="100"><br>
   <textarea name="ejemplo" cols="50" rows="10" id="ejemplo"></textarea><br>
   <input onclick="fakeaction()" type="button" name="Submit" value="Enviar">
 </form>
</body>
</html>

He implementado además el mecanismo que borra las copias de validador.php que tengan más de 10 minutos de antigüedad, ajusta el valor que tú creas adecuado.

Cuando intentes ver el resultado con el navegador el servidor se quejará con un "Warning" diciendo que no existe el archivo "validador.php". Calma, todavía no llegamos a eso. Falta una cosa más...

Implementando un "Captcha".

Yo considero que la solución expuesta en la sección anterior es suficiente para detener a los spambots, sin embargo ésta solución no es adecuada para todos los sitios webs; especialmente para aquellos que tienen mucho tráfico o hacen uso intensivo de formularios webs tal como los blogs. Para usos más moderados como formularios de contactos sí lo es, hasta cierto punto. Basta que por algún motivo una centena de usuarios traten de enviar correo legítimo desde un formulario creado con esta técnica para que las cosas no funcionen como deberían. El control de concurrencia es muy burdo. El usuario podría permanecer escribiendo en el formulario más que el tiempo límite establecido, la llegada de un nuevo usuario invalidaría al usuario anterior. Pero tampoco podemos establecer un tiempo límite muy largo pues eso haría inútil el mecanismo de un solo uso frente a los spambots. Y créanme, cuando un spambot ligado a una botnet detecta que ha podido enviar exitosamente un correo electrónico a través de un formulario web la botnet completa tratará de exprimir ese formulario hasta sus últimas consecuencias.

Por lo tanto vamos a implementar el CAPTCHA.

Necesitamos crear una imagen aleatoria. La biblioteca GD de PHP nos sirve para esto. Aunque a decir verdad es bastante engorroso hacerlo, hay que llamar muchas funciones de la biblioteca y sobre todo saber todas las consideraciones necesarias acerca del formato de la imagen que nos proponemos crear. Vamos a ahorrarnos esa tarea y usar una biblioteca ya hecha por alguien más.

En honor a la verdad la biblioteca que les voy a referir no fue hecha para hacer "captchas" sino para generar banners con texto en imágenes pero para nuestro propósito es lo mismo. Después de todo lo que queremos es solo mostrar unas cuantas letras y números dentro de una imagen, ¿cierto?.

La biblioteca de la que les hablo es Dynamic Text Replacement cuyo autor es Stewart Rosenberger. La intención del autor con esta biblioteca es la de reemplazar el uso del tag "font", el cual solo permite usar tipografías que ya existen en la máquina donde se ejecuta el navegador del visitante de una página web. Con su biblioteca podemos crear banners y textos con tipos de fuentes artísticas. Realmente vale la pena descargarla solo por ésta utilidad.

El archivo que quiero que descarguen de la página de Stewart Rosenberger está aquí:

http://www.stewartspeak.com/projects/dtr/ (link muerto, una lástima)

El dtr.zip de 5.5 KB. Dentro encontrarán un archivo que se llama heading.php.txt. Ese es el archivo principal de esta utilidad y es el que vamos a modificar.

Actualización: debido a que la página de Dynamic Text Replacement ya no está en línea, dejo el archivo en cuestión aquí.

¿Ya tienen el archivo?, pues qué bien; denle las gracias al autor, disfruten de su funcionalidad y ahora vean mi versión modificada la cual les ahorrará el trabajo de hacerlo uds. ;).

Originalmente el script espera recibir un parámetro llamado "text" y devuelve una imagen PNG. Para usarlo hay que reemplazar el parámetro "src" del tag "img" así:

Ejemplo de uso de heading.php
<img src="heading.php?text=esto es el texto">

A su vez es requisito subir al servidor el archivo .ttf, es decir el archivo de tipografía true type que se va a usar para mostrar el texto en cuestión, y asignar a la variable $font_file con el nombre de ese archivo.

Además:

$font_size es el tamaño en puntos de tipografía que se usará para mostrar la imagen.

$font_color es el color del texto expresando en formato hexadecimal estandar HTML.

$background_color es el color del fondo de la imagen.

$transparent_background es una variable booleana que indica si el fondo será transparente (wow!).

$cache_images sirve para indicarle al script que busque en caché imágenes ya generadas y las devuelva en vez de generarla nuevamente, esto sirve para acelerar el proceso. Para nuestro propósito *no debemos usar caché*.

$cache_folder es el nombre del directorio en el servidor donde se almacenarán y leerán las imágenes cacheadas.

Para nuestro propósito he modificado esto como sigue (lo pueden ver en la versión modificada):

Modificaciones 1
$colors = Array("#0000FF","#000000","#009900","#FF0000","#990099");
$font_file = "times.ttf";
$font_size = 12 ;
$font_color = $colors[rand(0,4)];
$background_color = '#ffffff' ;
$cache_images = false ;
$cache_folder = 'cache' ;
$text = "";

He creado un array de colores para luego elegir uno de ellos aleatoriamente. He asignado la tipografía "Timer New Roman" (cuestión de gustos, vaya...). Puesto el tamaño 12. Hago la selección de un color. Pongo el color de fondo a blanco. No cacheo imágenes. El directorio de caché lo dejo como está y limpio la variable $text que en el script original es la variable que recibe el contenido del parámetro "text" (pueden mirar más adelante que se usa $_GET para tomar el valor de ese parámetro).

Como mi propósito es usar este script en la implementación de un "captcha" no debo descubrir el texto en claro, ni siquiera como parte del código fuente HTML, cuáles son las letras que el usuario debe copiar viendo la imagen, por lo tanto no puedo usar el parámetro "text" de este script tal como fue diseñado originalmente. Antes que eso uso una variable de sesión. El valor de esta variable la genero en formulario.php, como veremos más adelante. Heading.php modificado por mi toma esa variable para generar el texto acorde.

Modificaciones 2
session_start();
$text = $_SESSION['captcha'];
$text = " ".$text." ";

A $text le agrego un espacio por delante y por detrás para compensar el ancho de la imagen generada.

Finalmente he modificado la parte del script que originalmente toma el parámetro "text", que se pasa a la variable $text, de forma tal que no lo haga, simplemente convirtiendo en comentario las tres líneas relevantes:

Modificaciones 3
// if(empty($_GET['text']))
// fatal_error('Error: No text specified.') ;
// $text = $_GET['text'] ;

Para usar la versión ya modificada es igual a como originalmente fue diseñado este script con la salvedad de que ya no hay que usar el parámetro "text". Pero la versión modificada no puede ser usada sin más. Hay que establecer el valor de la variable de sesión "captcha" a algo. Esto lo haremos reusando la función RandomLetters que implementamos en la versión 1.3 de formulario.php. Y además tenemos que inicializar la sesión para a su vez poder usar el array $_SESSION de PHP. Pero ¿por qué hay que hacerlo de esta manera?, pues porque validador.php tiene que saber de alguna forma cuál es el "captcha" correcto para poder compararlo con lo que ingresó el usuario.

El proceso es así.

  1. Se genera una cadena de letras aleatorias.
  2. Se crea una imagen con esas letras.
  3. Esas letras se guardan además en una variable de sesión, aprovechando que éstas residen en el servidor y son únicas para la cada conexión activa (según la documentación de PHP por omisión duran 60 minutos luego de ser creadas.
  4. El usuario envia el formulario supuestamente con el campo apropiado rellenado.
  5. Validador.php compara el campo del formulario con la variable de sesión.

El "truco" aquí está en aprovechar el mecanismo de sesión de PHP. Ésto funciona internamente creando un identificador único por cada sesión de usuario de un sitio web. Por cada usuario que visita el sitio se crea ese identificador que permanece igual sin importar qué página en concreto esté descargando del servidor. La sesión finaliza cuando no se detecta actividad por parte de ese usuario transcurridos 60 minutos. A ese identificador único se le asigna un espacio de variables, ese espacio de variables está representado en PHP por el array $_SESSION. En ese array nosotros podemos crear variables y usarlas sin que sus valores se pierdan de script a script (una suerte de variables globales creadas "al vuelo") teniendo la seguridad de que esas variables tienen significado para una sesión en particular. Así es como trasladaremos la cadena de letras aleatorias de formulario.php a validador.php más tarde.

Vamos a agregar las cosas que faltan a formulario.php para terminar de armar este tinglado.

Primero hay que generar la cadena de caracteres aleatoria. En mi caso haré que el visitante tenga que escribir cuatro letras:

Sesión de formulario.php
session_start();
$letras = strtoupper(RandomLetters(4));
$_SESSION['captcha'] = $letras;

Dentro del "form1" hay que agregar un input donde el usuario pueda tipear los caracteres de la imagen y hay que poner el img adecuado:

Captcha en formulario.php
<img src="heading.php" alt="escribe estas letras"><br>
<input name="captcha" type="text" id="captcha" size="4" maxlength="4"><br>

Notarán además que el "input" está limitado a 4 caracteres.

formulario.phps Pueden descargar la versión final de formulario.php aquí.

Procesando los datos.

La premisa para procesar los datos que llegan desde el formulario es ésta: no enviar el mensaje a menos que estemos absolutamente seguro de que todos los datos son razonablemente correctos. Esto quiere decir que a la menor señal de que algo está mal hay que abortar el proceso.

Para lograr esto vamos a validar todos los datos de los que disponemos, comenzando por los más generales y yendo escalonadamente hacia los más concreto.

Pero antes debemos decidir a dónde enviar al visitante en caso de encontrar un error o inconsitencia en los datos proporcionados. En mi caso simplemente enviaré al visitante de regreso al formulario. Uds. pueden adaptar esto como mejor los parezca. Usaré la función header() de PHP para enviar una cabecera redirect. Ésta cabecera necesita el URL completa para que funcione, por lo tanto también necesito extraer la URL completa donde reside nuestro script. Ésta es la forma de hacerlo:

Obtener la URL completa
$host  = $_SERVER['HTTP_HOST'];
$uri  = rtrim(dirname($_SERVER['PHP_SELF']), '/\\');
$base = "http://" . $host . $uri . "/formulario.php";

En $base tengo la URL completa hacia el script formulario.php incluyendo la ruta de directorios.

La primera validación que debemos hacer es comprobar que la petición hacia "validador.php" (o mejor dicho su copia pero para el caso es lo mismo) sea de tipo "POST" y ninguna otra. En PHP el tipo de petición la podemos determinar consultando la matriz superglobal $_SERVER["REQUEST_METHOD"].

Comprobar la petición
if ($_SERVER["REQUEST_METHOD"] != "POST") { header( "Location: $base" ); exit; }

Si la petición no es "POST" enviamos al visitante de regreso al formulario y abortamos el script.

Seguros de que es una petición "POST" ahora vamos a comprobar que el referer de nuestro script es el correcto. En este caso asumiré que solo se puede llamar a "validador.php" desde cualquier parte de nuestro sitio web, para ello comprobaré que la URL del referer contiene nuestro nombre de host. Este código se puede mejorar creando una lista de host permitidos y comparando el host del referer con esa lista. Pero antes de hacer esto vamos a comprobar que realmente exista un referer.

Validar "referer"
$ref = @$_SERVER["HTTP_REFERER"];
if (empty($ref)) { header( "Location: $base" ); exit; }
$parseref = parse_url($ref);
$alowedref = parse_url("http://".$host);    // ver $host más arriba.
if ($parseref["host"] != $alowedref["host"]) { header("Location: $base"); exit; }

Tomamos el valor del referer; el @ delante de la variable impide que PHP genere un mensaje de error en caso que esa variable no esté definida, la variable de destino de la asignación entonces queda vacía, que es lo que preguntamos luego, que a su vez es lo mismo que preguntar si no hay referer a nuestro script, en ese caso, redireccionamos al visitante y abortamos el script. En caso de que sí haya un referer, lo parseamos usando la función parse_url que divide una URL en sus componentes (consultar la ayuda), también parseamos la URL de nuestro script para finalmente compararlos entre sí, en caso de no coincidir, abortamos el script.

Hasta aquí hemos eliminado cualquier intento de uso inapropiado de nuestro script validador. Ahora pasemos a los campos del formulario.

Cada elemento del formulario que está en formulario.php tiene un atributo "name" al cual se le asigna un valor. El navegador debe armar una petición POST con los datos recogidos en esos campos, cada dato se identifica con el valor del atributo name, que a su vez nosotros podemos extraer de la matriz superglobal $_POST de PHP. A cada campo lo trasferiremos a una variable para trabajar con ella. En caso de no existir alguno de esos campos, como siempre, abortamos el script.

Probar que un campo no esté vacío
$nombre = @$_POST["esto"]; // el nombre del visitante
if (empty($nombre)) { header("Location: $base"); exit; }

Recordemos que el primer campo del formulario será llenado con el nombre del visitante. El resto del código no necesita más explicación.

El resto de los campos
$email = @$_POST["esun"]; // la dirección de correo electrónico
if (empty($email)) { header("Location: $base"); exit; }
$mensaje = @$_POST["ejemplo"]; // el cuerpo del mensaje
if (empty($mensaje)) { header("Location: $base"); exit; }
$captcha = @$_POST["captcha"]; // las cuatro letras mágicas :)
if (empty($captcha)) { header("Location: $base"); exit; }

Ahora estamos seguros que todos los campos contienen algo. Pasemos a verificar que ese "algo" tiene el tamaño correcto. Recordemos que en el formulario le habíamos puesto límite de caracteres a alguno de ellos. Un visitante legítimo hipotéticamente no podría poner más caracteres de lo que el atributo "maxlenght" le permite, pero un spambot podría saltarse eso fácilmente.

Validando el tamaño de los campos
/* "nombre" no puede ser mayor a 100 caracteres */
if (strlen($nombre) > 100) { header("Location: $base"); exit; }

/* "email" tampoco */
if (strlen($email) > 100) { header("Location: $base"); exit; }

/* el cuerpo del mensaje no puede ser mayor a 500 */
if (strlen($mensaje) > 500) { header("Location: $base"); exit; }

/* y el "captcha" no puede ser mayor a 4 caracteres */
if (strlen($captcha) > 4) { header("Location: $base"); exit; }

Lo que sigue sería probar que los campos contengan datos con algún sentido. Vamos a alterar el orden de validación en este paso y comenzaremos probando el "captcha", ya que sería inútil verificar el resto si éste campo en particular no coincide con lo que le hemos pedido al usuario que ingrese en el formulario.

Recordemos que en "formulario.php" habíamos establecido una variable de sesión que contiene las cuatro letras que le mostramos al visitante en una imagen.

Validando el CAPTCHA
/* reabrimos la sesión */
session_start();
/* pasamos las letras a mayúsculas */
$intcaptcha = strtoupper($_SESSION["captcha"]);
$captcha = strtoupper($captcha);
/* y comparamos */
if ($intcaptcha != $captcha) { header("Location: $base"); exit; }

Llegado a este punto podemos estar seguros que quien ha enviado el formulario es un ser humano. Vamos a verificar que la dirección de correo esté bien formada.

Esto merecería una discusión aparte ya que se presta a mucha confusión, aquí lo comentaré brevemente. No es suficiente con verificar que la cadena contenga un arroba en alguna parte o que además no contenga espacios. Tampoco que contenga un arroba y al menos un punto. Y es una mala idea descartar como dirección no válida aquellas cadenas que contengan caracteres especiales. ¿Por qué?, porque el RFC822 indica que lo que precede al arroba (es decir, el "nombre de usuario") puede ser cualquier cosa, incluso un espacio en blanco siempre y cuando éste esté indicado como tal. También puede aparecer una comilla simple o doble. Por lo tanto, restringir a que la cadena que se supone es una dirección de correo electrónico a solo un subconjunto de caracteres ASCII no es lo correcto. Sin embargo nadie crea un nombre de usuario con espacios en blanco o comillas pues presenta algunos problemas con otros protocolos. En el protocolo HTTP las comillas dobles tienen significado especial como así también el espacio en blanco y requieren tratamiento especial, como veremos más adelante.

Entonces aquí hay una función en PHP para validar una dirección de correo acorde al RFC822.

Función is_email
function is_email($Addr) {
   $p = '/^[a-z0-9!#$%&*+-=?^_`{|}~]+(\.[a-z0-9!#$%&*+-=?^_`{|}~]+)*';
   $p.= '@([-a-z0-9]+\.)+([a-z]{2,3}';
   $p.= '|info|arpa|aero|coop|name|museum)$/ix';
   return preg_match($p, $Addr);
} // is_email

Parece un galimátias, ¿eh?. Esta función devuelve "true" (verdadero) en caso de que la cadena pasada como parámetro sea una dirección de correo electrónico correctamente bien formada.

Entonces validamos el campo "email" del formulario:

Validando la dirección de correo electrónico
if (!is_email($email)) { header("Location: $base"); exit; }

Sin embargo debo señalar que el hecho de que una dirección de correo electrónico está bien formada, no quiere decir que la dirección sea válida, con "válida" quiero decir que esa dirección exista. Por ejemplo, una dirección tal como: mi_usuario@example.com.ar pasará la prueba de más arriba, a pesar de que esta dirección es ficticia. Hay una forma de controlar esto aunque no la implementaré aquí. Se trata de tomar la parte del dominio de la dirección (lo que está a la derecha del @) y probar si ese dominio existe usando la función estandar de PHP gethostbyname() que recibe como parámetro un nombre de dominio y devuelve su correspondiente número de IP, o la propia cadena pasada en caso de error. Esto se lo dejo como ejercicio al lector.

Previamente había comentado el problema que surge con las comillas dobles. Cuando se quieren pasar datos a través del protocolo HTTP que contienen estas comillas se tienen que escapar antecediéndole una barra inclinada. Nosotros recibiremos en nuestro script algo como esto: \" (barra-comillas) cuando el usuario ha escrito esto: " (solo comillas). Algo similar pasa con la comilla simple.

Si nuestra intención es enviar un mensaje vía SMTP es necesario tratar esto simplemente quitando esas barras:

Quitando las barras
      $mensaje = str_replace("\'","'",$mensaje);
      $mensaje = str_replace('\"','"',$mensaje);

Hemos terminado de validar los datos que vienen del formulario. Estamos listos para enviar todo esto vía SMTP.

Enviando correo electrónico vía SMTP.

En PHP existe una función estandar para enviar correo electrónico: mail(). Que es, lamento decir, bastante complicada de usar ya que por su simpleza requiere que uno sepa armar cabeceras de correo electrónico y además es muy insegura debido a que basta que un visitante de nuestra página agregue palabras clave como "to:" y "from:" para que esta función asuma que lo que sigue es parte de la cabecera del mensaje que queremos enviar. Todas las validaciones que hicimos en la sección anterior de este artículo no evitan esto. Debido a estas consideraciones y otras más que no comentaré excepto el hecho de que no todos los servidores de hosting de páginas web tienen habilitada esta función (por el peligro que esto encierra) les aconsejo no usar esta función. En su reemplazo les recomendaré un script ya hecho que no hace uso de esta función sino que tramita todo el protocolo SMTP directamente con el servidor de correos, cualquier servidor, mientras en ese servidor tengamos una cuenta de correo válida. Esa dirección tiene que ser la dirección de destino de nuestro formulario.

El script en cuestión es el Swift PHP mailer cuyo autor es Chris Corbyn. Este script tiene las ventajas de que facilitan la formación del mensaje de correo, permite enviar correo en formato HTML, incluyendo imágenes dento del mensaje, no hace uso de mail() como ya mencioné y es gratuito :D. Sugiero entonces que descarguen el script y lo instalen en su hosting. Aquí presento cómo debería ser el script validador.php haciendo uso de Swift de la forma más sencilla que este script permite.

Enviando correo vía SMTP con Swift
/* Comienza la parte de Swift */
require('Swift.php');
require('Swift/Connection/SMTP.php');

/* Cuerpo del mensaje en texto plano */
$PlainText = "Nombre: " . $nombre;
$PlainText = $PlainText . "\r\nCorreo: " . $email;
$PlainText = $PlainText . "\r\n" . $mensaje;

/* Cabeceras necesarias */
$MailSender = $nombre . "<" . $email . ">";
$MailRecipient = "yomismo@midominio.com.ar";
$Asunto = "Mensaje desde mi web: " . $nombre;

/* Datos de la conexión al servidor SMTP */
$connection = new Swift_Connection_SMTP('mail.midominio.com.ar','25');
$mailer = new Swift($connection);

/* Agregamos el cuerpo del mensaje */
$mailer->addPart($PlainText);
 
/* Conectando con el servidor */
if ($mailer->isConnected())
{
/* Verificando el login */
  if ($mailer->authenticate('yomismo@midominio.com.ar','micontraseña'))
  {
    /* Si se conectó, se envia el correo */
    $mailer->send($MailRecipient,$MailSender,$Asunto);
  }
    /* Se cierra la conexión */
  $mailer->close();
}
else {
echo "Algo salió mal. Errores: ".print_r($mailer->errors, 1);
echo "Log: ".print_r($mailer->transactions, 1);
}
/* Termina la parte de Swift */

El script Swift sería, en el mecanismo de los tres pasos, lo que yo llamé "enviador.php". Éste es el script que nunca debe reverlarse al navegador del usuario o visitante de nuestro sitio web y que solo debe ser accedido a través de "validador.php".

Por último, ya que hemos podido enviar el mensaje, hay que informárselo al visitante.

Nuestras felicitaciones al visitante
$enviado = "http://" . $host . $uri . "/enviado.htm";
header( "Location: $enviado" );

El script "validador.php" completo tal como lo expliqué aquí puede ser descargado aquí.

Consideraciones finales.

Aunque a primera vista todo esto parece ser formidable hay detalles que se pueden mejorar, a lo ya mencionado acerca de la validación de referer habría que agregar la posibilidad de poder enviar mensajes a diferentes cuentas según el formulario que el usuario haya usando en nuestro sitio web, eso podría hacerse agregando un elemento input de tipo "hidden" en cada formulario de nuestro sitio con un valor que luego analizaremos en "validador.php" para determinar a qué cuenta hay que enviar el mensaje. E insisto en que detectar un campo no válido según nuestro criterio es necesario descartar todo el envio.

Por Diego Romero,