Detectar un formulario de contacto suena a problema resuelto hasta que se apunta a mil sitios de empresas. Las fallas no son exóticas. Hay un formulario y el parser no lo ve. El parser ve nueve campos y escribe el mensaje en el equivocado. La guarda que debía detenernos ante un formulario protegido lee una propiedad que no existe, recibe undefined y deja pasar todo. A una vía de contacto pública utilizable le decimos puerta, y esto es lo que implica de verdad encontrar una.
01La detección de formularios de contacto empieza por la plataforma
Primero la buena noticia: los formularios de contacto no son una cola larga. En las empresas que leemos, cinco cosas explican casi todo lo que aparece. HubSpot, ActiveCampaign, WPForms, Typeform y un formulario HTML plano que publica de vuelta al sitio donde vive. Identifique la plataforma primero, y después corra el extractor escrito para ella. Una pasada genérica que junta todos los campos de la página se equivoca en al menos tres de las cinco, y se equivoca sin levantar nada.
- HubSpot: la página carga un identificador de portal y un guid de formulario, y nada de lo que usted envíe va a la página que está mirando. El envío es un POST de JSON a un endpoint regional indexado por el portal, y la región es parte de la dirección y no un encabezado, así que un portal europeo enviado al host por defecto no lo acepta nadie. La lista de campos sale de la definición del formulario, no del DOM.
- ActiveCampaign: el embebido es una etiqueta de script, y el marcado vive dentro de una cadena de JavaScript, con doble escape. Los signos de mayor y menor llegan como escapes unicode y toda comilla lleva barra invertida. Un parser de HTML apuntado a esa página no encuentra formulario alguno, porque en ese momento no hay uno. Desescape la cadena, parsee el resultado, y extraiga del resultado.
- WPForms: los nombres son posicionales. Un campo se llama wpforms[fields][3] y el 3 es el identificador de una fila en la base de WordPress de otra persona, así que el nombre no carga significado. Lo cargan en su lugar el texto de la etiqueta y el atributo de tipo del campo.
- Typeform: no hay formulario en la página. Hay un flujo alojado descrito como bloques de JSON, y la lectura honesta es que esta no es una puerta que podamos usar.
- HTML plano: un action, un method y por lo general un token oculto que hay que devolver junto con la cookie de sesión que lo emitió. La descarga y el envío tienen que compartir el mismo tarro de cookies o el post se rechaza como falsificación, que es el comportamiento correcto de parte del sitio.
Cada uno de esos es un contrato de envío y no una rareza de renderizado. El extractor de uno no es una versión ligeramente ajustada del extractor de otro, y fingir lo contrario produce un detector que reporta éxito en casi toda página y una tasa de entrega que no le da la razón.
02El mapeo de campos es donde se pone difícil
Digamos que el formulario se encontró y se parseó. Ahora usted tiene entre cuatro y once campos y un mensaje que colocar. ¿Cuál casilla es el mensaje?
Puntuamos cada campo por nombre, id, placeholder, texto de la etiqueta asociada, aria-label, tipo de elemento y maxlength, y después tomamos la mejor puntuación por encima de un umbral. Un textarea es la señal individual más fuerte y aun así no es decisiva: muchísimos formularios usan un textarea para una dirección postal, y algunos usan un campo de una línea con un maxlength de 500 para el mensaje. Asunto y mensaje son el par que más se confunde. Los dos son texto, están uno junto al otro, y cambiarlos le da a quien recibe una línea de asunto de cuatro párrafos.
Después están los campos que hay que dejar en paz. Un honeypot es un campo oculto a una persona y dejado visible para un script ingenuo, y llenarlo es exactamente como el sitio se entera de que usted es un script. Marcamos un campo como honeypot ante cualquiera de estas: oculto por display o visibility, posicionado fuera de la pantalla, opacidad cero, un tabindex negativo, aria-hidden, o un nombre del repertorio habitual (url, website, hp, _gotcha, o comment donde ya existe un campo de comentario). Un campo marcado se envía exactamente como lo encontramos. Eso no es evadir nada. Es llenar el formulario como lo llenaría el navegador que tiene enfrente una persona.
Por debajo del umbral no adivinamos. No hay puerta, la lectura queda en espera con el motivo escrito, y aparece en el registro de rechazos como rechazada y no como un intento silencioso. Publicar un párrafo en el campo etiquetado "¿Cómo se enteró de nosotros?" es peor que no publicar nada, porque gasta la atención de una persona desconocida dentro de un formulario al que no puede responder.
03Dos errores que fueron silenciosos justo del modo equivocado
El primero es lo peor que hemos publicado. El detector devuelve un resultado anidado y el hallazgo del CAPTCHA vive en result.form.captcha. La guarda leía result.captcha. Esa propiedad es undefined, undefined es falso, y la guarda concluía por tanto que ningún formulario en ninguna parte tenía CAPTCHA. Nada lanzó una excepción. Nada quedó en la bitácora. Los formularios protegidos pasaron durante casi toda una tarde, y salió a la luz solo porque una verificación manual contra un formulario que sabíamos protegido volvió limpia y nadie creyó el resultado.
El arreglo es una regla y no una ruta de propiedad corregida. La guarda ahora exige un false explícito para poder continuar. Cualquier otra cosa detiene el trabajo: undefined, null, un objeto faltante, una cadena, un valor verdadero. Afirmamos la forma del resultado del detector en la frontera en lugar de leer a través de él con optimismo, y hay una prueba que le entrega a la guarda un resultado con la bandera movida un nivel más adentro y espera que se detenga. Falla contra el código viejo, que es la única definición útil de una prueba de regresión.
El segundo error es menor y costó más horas. La coincidencia de campos permitía coincidencia por sufijo, así que un campo llamado contact[email] y uno llamado contact[secondary_email] satisfacían ambos una regla que buscaba un campo de correo, y ganaba el que viniera último en el orden del documento. En un formulario donde la dirección secundaria era opcional y sin validación, mapeamos un campo obligatorio sobre el opcional. El envío tuvo éxito, el sitio devolvió una página de agradecimiento, y el mensaje aterrizó en un campo que nadie lee.
Tres reglas lo resuelven ahora, aplicadas en orden. Una coincidencia exacta de nombre le gana a una por sufijo y termina la búsqueda. Obligatorio le gana a opcional, leído del atributo required, de aria-required y de la definición de campo de la plataforma cuando la plataforma la ofrece. Los empates se resuelven por la posición más temprana en el documento. El mapeo resultante se escribe en el registro de auditoría, así que la persona que aprueba el borrador puede ver en qué campo aterrizará cada valor antes de aprobar y no después.
04Dónde nos detenemos
Nunca resolvemos ni evadimos un CAPTCHA. Ni con un servicio que los resuelva, ni con un navegador afinado para parecer más humano, ni buscando el endpoint viejo sin proteger que está detrás del protegido, que es el mismo acto ejecutado con mejores modales. Cuando el detector reporta un CAPTCHA el trabajo pasa a needs_review y ahí se queda hasta que una persona lo abra. Casi siempre nadie lo hace, y eso es un resultado y no una falla.
Un CAPTCHA en un formulario de contacto es una preferencia declarada. Quien es dueño dijo, en casi el único vocabulario disponible para decírselo a un software, que quiere a una persona del otro lado. Nosotros nos dedicamos a pedirle a desconocidos que consideren comprar algo, y esa conversación no abre bien demostrando que sus preferencias no nos obligan.
Algunas otras cosas tampoco califican como puertas. Un formulario detrás de un inicio de sesión no es público. Un formulario de tickets de soporte es el público equivocado, porque es una fila atendida para clientes que pagan y un mensaje de ventas ahí le cuesta a alguien su tarde. Un registro a boletín no es una vía de contacto. Cada uno de esos queda anotado como una lectura rechazada con un motivo escrito, el mismo trato que recibe una empresa por no pasar un filtro de encaje.
Hoy la entrega ocurre por el propio formulario de contacto público de la empresa objetivo y por nada más. El envío de correo está planeado para el cuarto trimestre de 2026 y no está construido, así que no hay vía de respaldo cuando falta una puerta ni segundo canal cuando el primero está protegido. Esa ausencia es lo que mantiene honesta la regla. Si existiera una alternativa fácil, detener el trabajo no costaría nada, y una regla que no cuesta nada no le dice nada.
Nada de esto es ingenioso. Es un detector de plataformas, una función de puntuación, dos guardas que fallan cerrando, y un motivo escrito adjunto a cada lectura sobre la que no actuamos. La versión ingeniosa de este sistema reportaría una cuenta más alta de envíos realizados y sería peor.