12k
All articles

Por qué no deberías validar emails con regex

Por qué falla la regex de correo: rechaza direcciones válidas, acepta otras no entregables y puede causar ReDoS. Usa mejor el input email de HTML5 o una librería.

OpenReplay Team
OpenReplay Team
Por qué no deberías validar emails con regex

Una regex no puede validar una dirección de email — solo puede verificar su forma aproximada, y hasta el patrón más cuidadosamente elaborado rechazará direcciones legítimas y aceptará otras sintácticamente plausibles que nunca llegarán a su destino. El problema no es que aún no hayas encontrado el patrón correcto; es que la pregunta “¿es este email válido?” combina tres problemas distintos, y una regex solo puede abordar uno de ellos — el menos útil. Este artículo separa esos tres problemas, cita lo que las especificaciones realmente dicen (RFC 5321, RFC 5322, RFC 6531 y el WHATWG HTML Living Standard), muestra exactamente dónde fallan los patrones más populares en ambas direcciones, y te proporciona el código pragmático en JS/TS que deberías usar en su lugar.

Puntos clave

  • La validación de email tiene tres capas diferenciadas — una verificación básica de UX, validación sintáctica contra RFC 5321/5322, y verificación de existencia — y solo un email de confirmación demuestra que la dirección realmente recibe mensajes.
  • El <input type="email"> del HTML Living Standard usa una regex que la propia especificación califica como una “violación deliberada del RFC 5322”; intencionalmente no es RFC-completa y es una mejor opción por defecto que cualquier patrón que escribas a mano.
  • Las regex de email más populares fallan en ambas direcciones: rechazan direcciones reales (plus-addressing, nuevos gTLDs, partes locales entre comillas, direcciones internacionalizadas según RFC 6531) y aceptan direcciones no entregables.
  • Una regex de email propensa al backtracking ejecutada en el lado del servidor en Node.js puede ser explotada con una entrada corta y manipulada para bloquear el event loop — un vector de denegación de servicio ReDoS (CWE-1333).
  • La única restricción de longitud que vale la pena aplicar antes de ejecutar cualquier patrón proviene del RFC 5321 §4.5.3.1.3: la dirección en sí está limitada a 254 caracteres.

Las tres capas de la validación de email

La validación de email tiene tres capas diferenciadas: una verificación básica de UX (¿tiene aspecto de email?), validación sintáctica (¿cumple las reglas del RFC 5321/5322?) y verificación de existencia (¿este buzón realmente recibe correo?). Solo la tercera capa demuestra que la dirección funciona, y únicamente un email de confirmación lo garantiza. Las referencias que recomiendan “dejar de usar regex” tienen razón, pero mezclan estas capas entre sí. Mantenerlas separadas es lo que te indica qué herramienta corresponde a cada caso.

  • Capa 1 — Verificación básica de UX. Una comprobación rápida y económica en el lado del cliente que detecta errores tipográficos obvios (alicegmail.com, un espacio al final) y proporciona retroalimentación inmediata. Esta es la única capa donde una regex tiene cabida, y aun así conviene usar el patrón más pequeño que haga el trabajo.
  • Capa 2 — Validación sintáctica. ¿La cadena cumple la gramática definida en los RFC de email? Esto es mucho más difícil de lo que parece, supera a las regex escritas a mano y — lo más importante — no demuestra nada sobre la entregabilidad. Una dirección perfectamente conforme al RFC puede apuntar a un dominio que no existe.
  • Capa 3 — Verificación de existencia. ¿Un buzón real recibe correo en esta dirección? La única prueba de que una dirección de email funciona es un mensaje entregado con éxito; un email de confirmación hace en un solo paso lo que ninguna regex puede hacer en absoluto.

El error que comete casi todo “regex definitiva para email” es intentar ejecutar la Capa 2 a la perfección, cuando la Capa 2 no responde la pregunta que realmente importa. La pregunta es la Capa 3, y ningún patrón llega hasta ahí.

Qué significa realmente “válido”

Una dirección de email válida es mucho más permisiva de lo que la mayoría de las regex asumen, porque la gramática del RFC 5321 (SMTP) y el RFC 5322 (el formato del mensaje) permite construcciones que parecen incorrectas. La parte local — todo lo que precede al @ — puede contener una larga lista de caracteres especiales e incluso puede ser una cadena entre comillas.

La parte local sin comillas se construye a partir de atext, definido en el RFC 5322 §3.2.3, que permite estos caracteres junto con letras y dígitos:

! # $ % & ' * + - / = ? ^ _ ` { | } ~

Esto significa que user+tag@example.com (plus-addressing) es válido — el + es atext ordinario, según el RFC 5321 §4.1.2. Si el servidor receptor trata el +tag como una subdirección es específico de la implementación (RFC 5233), pero la dirección en sí está bien formada. La parte local también puede ser una cadena entre comillas: "user name"@example.com es válida según el RFC 5321 §4.1.2 y el RFC 5322 §3.2.4, espacios incluidos. El dominio puede ser una dirección IP literal entre corchetes — user@[192.168.1.1] es válida según el RFC 5321 §4.1.3.

Existe una restricción que vale la pena aplicar de forma económica. El RFC 5321 §4.5.3.1.3 limita el forward-path a 256 octetos incluyendo los corchetes angulares, lo que deja 254 caracteres para la dirección en sí; la parte local está limitada a 64 octetos (§4.5.3.1.1) y el dominio a 255 (§4.5.3.1.2). Una comprobación de longitud es la única validación que una comparación de cadenas maneja correctamente y que una regex no necesita realizar.

Direcciones internacionalizadas (EAI)

Las direcciones de email internacionalizadas definidas en el RFC 6531 — como 用户@例子.广告 — son válidas y cada vez más comunes; ninguna regex exclusivamente ASCII las gestiona correctamente, y esto es un problema de librería, no de regex. EAI (RFC 6531 §3.3) extiende la parte local para permitir UTF-8, y el dominio puede ser Unicode no ASCII. Esto es distinto de los dominios codificados en punycode IDNA (RFC 5891): EAI abarca también la parte local. Cualquier patrón que asuma [a-zA-Z0-9] para la parte local es incorrecto para una porción creciente de los usuarios del mundo, y no existe una única regex que acepte correctamente tanto partes locales ASCII como Unicode sin aceptar también cadenas inválidas.

Por qué las regex de validación de email fallan en ambas direcciones

Una regex de email escrita a mano falla tanto como filtro de entrada como de salida: produce falsos negativos (rechazando direcciones entregables) y falsos positivos (aceptando direcciones que cumplen la gramática pero que nunca recibirán correo). Ambos modos de fallo llegan constantemente a producción porque el conjunto de pruebas usa test@example.com, que pasa todos los patrones.

Tomemos el clásico copy-paste de Stack Overflow — un patrón que requiere un TLD de 2 a 4 caracteres:

// Un patrón habitualmente copiado y pegado. No uses esto.
const bad = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/;

Esto es lo que hace con direcciones reales:

DirecciónQué pruebaEsta regexResultado correctoPor qué es incorrecto
name+filter@gmail.complus-addressing✅ acepta✅ válida(pasa aquí, pero patrones más estrictos rechazan +)
user@studio.photographygTLD largo❌ rechaza✅ válida{2,4} rechaza TLDs de más de 4 caracteres
"user name"@example.comparte local entre comillas❌ rechaza✅ válidalas cadenas entre comillas y los espacios son válidos
用户@例子.广告EAI (RFC 6531)❌ rechaza✅ válidaclases de caracteres solo ASCII
someone@validformat.testdominio inexistente✅ acepta❌ no entregablela sintaxis es correcta; el dominio no resuelve

Una regex que rechaza name+filter@gmail.com (plus-addressing) o user@studio.photography (un gTLD delegado en el Programa de Nuevos gTLDs de ICANN, con .photography añadido a la raíz en 2013) no está siendo estricta — está siendo incorrecta. Ambas son direcciones sintácticamente plausibles que utilizan características válidas del email. La restricción {2,4} del TLD por sí sola rompe .photography, .accountants, .engineering y cientos de otras delegaciones válidas.

Las grabaciones de sesiones revelan con frecuencia usuarios que encuentran errores de validación, corrigen su entrada múltiples veces y acaban abandonando el formulario. Los estudios sobre usabilidad de formularios han identificado consistentemente la fricción en la validación como un factor que contribuye al abandono y a la reducción de las tasas de conversión.

Los falsos positivos — direcciones que pasan la regex pero que nunca llegan a su destino — son igualmente reales. someone@validformat.test pasa el patrón anterior y la mayoría de los demás, sin embargo .test es un TLD reservado (RFC 2606) que nunca entregará nada. La conformidad sintáctica y la entregabilidad son propiedades independientes, y una regex solo ve la primera.

ReDoS: cuando la regex es la vulnerabilidad

Una regex propensa al backtracking ejecutada en el lado del servidor en Node.js puede ser explotada con una entrada manipulada para bloquear el event loop — un vector de denegación de servicio (CWE-1333: Inefficient Regular Expression Complexity, y la referencia OWASP ReDoS) que no tiene nada que ver con el email y sí mucho con el backtracking catastrófico. Los patrones con cuantificadores anidados o adyacentes sobre clases de caracteres superpuestas pueden tardar un tiempo exponencial en entradas que casi coinciden.

A continuación se muestra una demostración reproducible. El (...)+ del patrón envuelve un grupo que puede coincidir con el mismo carácter de múltiples formas, por lo que una larga secuencia de un único carácter seguida de un carácter que no coincide obliga al motor a probar exponencialmente muchas particiones antes de fallar:

// Node.js v24. Ejecutar con: node redos.js
// Un patrón deliberadamente vulnerable, propenso al backtracking.
const evil = /^([a-zA-Z0-9]+)*@example\.com$/;

// Una entrada casi válida manipulada: muchas 'a', luego un carácter que rompe la coincidencia.
const attack = "a".repeat(40) + "!";

console.time("redos");
evil.test(attack); // bloquea el event loop
console.timeEnd("redos");

En una versión actual de Node.js, aumentar el número de repeticiones hace que el tiempo de coincidencia crezca de forma explosiva — cada carácter añadido aproximadamente duplica el trabajo. Dado que el motor de regex de Node se ejecuta de forma síncrona en el hilo principal, una única solicitud con esta entrada paraliza el event loop y bloquea todas las demás solicitudes en curso. La forma (x+)* es la señal de alerta: cualquier grupo que pueda coincidir con la misma subcadena de más de una manera, bajo un cuantificador externo, es candidato a un backtracking catastrófico. La solución no es un patrón más inteligente — es no construir este tipo de patrón en absoluto, que es exactamente lo que se consigue delegando en la plataforma o en una librería mantenida.

La sintaxis no es entregabilidad

Incluso una dirección perfectamente conforme al RFC no te dice nada sobre si el correo llegará a su destino. Una regex no puede verificar que el dominio existe, que tiene registros MX, que el buzón está aprovisionado, o que la dirección no es un alias desechable. Estas son preguntas de red y de política, no de gramática. Una dirección como realuser@gmail.com y un error tipográfico como realuser@gmial.com son ambas sintácticamente válidas; solo una consulta DNS las distingue, y solo una entrega real distingue un buzón activo de uno inactivo.

Los dominios de email desechables y temporales son una preocupación relacionada pero separada: direcciones que son sintáctica y operativamente válidas pero que existen para evadir tu proceso de registro. Detectarlas requiere una lista de bloqueo mantenida de dominios proveedores, no un patrón — la lista de dominios cambia constantemente, y cualquier lista que codifiques directamente quedará obsoleta. Trátalo como una capa de política por encima de la validación, no como parte de ella.

Qué hacer en su lugar

Usa el enfoque por capas: una comprobación mínima de sanidad para la UX, la validación integrada de la plataforma para la sintaxis, una librería mantenida solo cuando necesites más, y un email de confirmación para lo único que realmente importa. Este es el orden, de más económico a más autoritativo.

1. Una comprobación mínima de sanidad

Para retroalimentación instantánea en el lado del cliente, el patrón más pequeño y útil es el del argumento original de “deja de validar con regex”: requiere algo, un @, algo, un punto, y algo más. Combínalo con una comprobación de longitud.

/**
 * Comprobación de sanidad de Capa 1: detecta errores tipográficos obvios, nada más.
 * Deliberadamente permisiva — NO es prueba de validez.
 * @param value - la cadena de entrada sin procesar
 * @returns true si el valor tiene la forma aproximada de un email y tiene <= 254 caracteres
 */
export function looksLikeEmail(value: string): boolean {
  if (value.length > 254) return false; // RFC 5321 §4.5.3.1.3
  return /.+@.+\..+/.test(value);
}

Esta comprobación de sanidad rechaza alicegmail.com y alice@localhost, acepta plus-addressing y gTLDs largos, y se ejecuta en tiempo constante. No es seguro tratar su true como “válido” — es un detector de errores tipográficos.

2. Prefiere la plataforma: <input type="email">

La mejor opción por defecto para la validación sintáctica es el propio <input type="email"> del navegador, y vale la pena entender exactamente qué hace. El <input type="email"> del HTML Living Standard usa una regex que la propia especificación denomina “violación deliberada del RFC 5322” — intencionalmente no es RFC-completa, sacrificando la precisión de la especificación en favor de la usabilidad, y es una mejor opción por defecto que cualquier patrón que escribas tú mismo. La especificación cita el patrón exacto:

^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$

Cláusula por cláusula:

  • [a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+ — la parte local, que permite los caracteres especiales de atext. Deliberadamente no admite partes locales entre comillas ("user name"@…).
  • @ — exactamente un separador.
  • [a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? — una etiqueta de dominio: comienza y termina con carácter alfanumérico, se permiten guiones en el interior, limitada a 63 caracteres. Deliberadamente no admite dominios IP-literal (user@[192.168.1.1]).
  • (?:\.<etiqueta>)* — cero o más etiquetas adicionales separadas por puntos, por lo que tanto dominios de una sola etiqueta como de múltiples etiquetas son válidos.

El WHATWG documenta abiertamente el compromiso: este patrón rechaza intencionadamente algunas direcciones RFC 5322 técnicamente válidas (partes entre comillas, literales IP) porque esas formas son extremadamente raras en registros reales y admitirlas introduce más errores de los que previene. Ese es el compromiso correcto para un campo de formulario, y por eso <input type="email"> debería ser tu línea base de Capa 2 — no tiene patología de backtracking y coincide con lo que los navegadores ya aplican.

3. Recurre a una librería mantenida solo cuando necesites más

Si necesitas validación sintáctica en el lado del servidor más allá del patrón HTML5, usa una librería mantenida y bien probada en lugar de crear la tuya propia. El paquete validator (npm validator, licencia MIT) expone una función isEmail que admite partes locales entre comillas y ofrece opciones para dominios IP-literal y nombres para mostrar:

import isEmail from "validator/lib/isEmail";

/**
 * Validación sintáctica de Capa 2, en el lado del servidor.
 * @param email - dirección candidata (ya verificada en longitud)
 * @returns true si es sintácticamente válida según las reglas alineadas con RFC de validator
 */
export function isSyntacticallyValid(email: string): boolean {
  return isEmail(email, { allow_utf8_local_part: true });
}

Prefiere este paquete sobre el antiguo email-validator, que no ha sido publicado desde 2018. Una librería te proporciona manejo probado de casos límite y un mantenedor activo que corrige los casos que tu patrón escrito a mano nunca contemplará — incluyendo, con las opciones correctas, direcciones EAI.

4. La respuesta real: envía un email de confirmación

El único paso que demuestra que una dirección funciona es la entrega. Envía un mensaje de confirmación con un enlace de un solo uso; considera la dirección como verificada solo después de que el usuario haga clic en él. Esto es el doble opt-in, y hace que la validación exhaustiva previa sea redundante — una dirección malformada o no entregable simplemente nunca se confirma.

/**
 * Esquema del flujo de verificación. El almacenamiento y el mailer son específicos de la aplicación.
 * @param email - una cadena que ya pasó las comprobaciones de longitud y sintaxis
 */
async function startEmailVerification(email: string): Promise<void> {
  const token = crypto.randomUUID();
  await storePendingVerification(email, token); // expira después de, p. ej., 24h
  const link = `https://app.example.com/verify?token=${token}`;
  await sendMail(email, "Confirma tu email", `Haz clic para confirmar: ${link}`);
  // Marca la cuenta como verificada solo cuando /verify recibe un token válido.
}

Enviar un email de confirmación es la estructura a la que toda fuente acaba llegando, por la misma razón: hace en un solo paso lo que ninguna regex puede hacer en absoluto. Como dijo Jamie Zawinski: “Algunas personas, cuando se enfrentan a un problema, piensan: ‘Ya sé, usaré expresiones regulares.’ Ahora tienen dos problemas.” Para el email, el segundo problema es que la regex sigue sin responder la pregunta.

Conclusión

Deja de intentar validar la dirección y empieza a verificar el buzón. Usa un patrón mínimo más un límite de 254 caracteres para retroalimentación instantánea de UX, apóyate en <input type="email"> o en una librería mantenida como validator para la sintaxis, y protege toda cuenta real con un email de confirmación — ese último paso es el único que demuestra que hay alguien al otro lado. La próxima vez que un formulario de registro necesite un campo de email, recurre a la plataforma y al flujo de confirmación, no al patrón de Stack Overflow.

Preguntas frecuentes

¿Cuál es la longitud máxima válida de una dirección de email?

Una dirección de email está limitada a 254 caracteres. Esto se deriva de la sección 4.5.3.1.3 del RFC 5321, que limita el forward-path a 256 octetos incluyendo los corchetes angulares circundantes, dejando 254 para la dirección en sí. La parte local está limitada por separado a 64 octetos y el dominio a 255 octetos. Una simple comparación de longitud aplica esto correctamente, y es la única validación que vale la pena realizar antes de ejecutar cualquier patrón.

¿El input de email de HTML5 valida contra la gramática completa del RFC 5322?

No. El HTML Living Standard describe explícitamente su regex de input de email como una 'violación deliberada del RFC 5322'. Rechaza intencionadamente formas técnicamente válidas como partes locales entre comillas ('user name'@example.com) y dominios IP-literal (user@[192.168.1.1]) porque son extremadamente raras en registros reales. El compromiso favorece la usabilidad sobre la completitud de la especificación, lo que lo convierte en una opción por defecto más segura que un patrón escrito a mano, pero no es un validador RFC completo.

¿Cómo puede una regex de validación de email causar un ataque de denegación de servicio?

Una regex con cuantificadores anidados o adyacentes sobre clases de caracteres superpuestas, como la forma ([a-zA-Z0-9]+)*, puede tardar un tiempo exponencial en entradas que casi coinciden. Esto es backtracking catastrófico, clasificado como CWE-1333. Ejecutada en el lado del servidor en Node.js, donde el motor de regex se ejecuta de forma síncrona en el hilo principal, una única solicitud manipulada puede paralizar el event loop y bloquear todas las demás solicitudes en curso. La solución es evitar por completo esta clase de patrones, no escribir uno más inteligente.

¿Puede una regex comprobar si una dirección de email realmente existe?

No. Una regex solo inspecciona la forma de la cadena; no puede verificar que el dominio existe, tiene registros MX, o que el buzón está aprovisionado. La conformidad sintáctica y la entregabilidad son propiedades independientes. Una dirección como realuser@gmial.com es sintácticamente válida pero no entregable debido a un error tipográfico, y someone@validformat.test pasa la mayoría de los patrones pero usa un TLD reservado que nunca entrega nada. Solo un email de confirmación entregado con éxito demuestra que una dirección recibe correo.

¿Por qué las regex de email rechazan direcciones válidas como name+filter@gmail.com?

El plus-addressing es completamente válido porque el signo más es `atext` ordinario según la sección 3.2.3 del RFC 5322 y la sección 4.1.2 del RFC 5321. Los patrones que lo rechazan, junto con direcciones en gTLDs largos como .photography o direcciones internacionalizadas definidas en el RFC 6531, no están siendo estrictos sino incorrectos. Estos falsos negativos llegan a producción porque los conjuntos de pruebas usan test@example.com, que pasa todos los patrones, por lo que el rechazo de direcciones reales nunca aparece en las pruebas.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.