Saltearse al contenido

Validación de formularios

La validación de formularios comprueba que los datos que entran a tu aplicación (el cuerpo de un POST, los parámetros de una query) tengan la forma correcta antes de llegar a tu servidor de origen, a tu sitio PHP o a una función Lambda. Definís el esquema una vez, con un formato declarativo estilo Zod, y Tero Services rechaza en el borde todo lo que no encaje.

La validación de formularios y el WAF son capas complementarias e independientes. Resuelven problemas distintos:

WAFValidación de formularios
Qué detectaAtaques (inyección SQL, XSS, traversal de paths)Datos de negocio inválidos (email mal formado, edad fuera de rango, enum no permitido)
Qué responde403 Forbidden, opaco422 Unprocessable Entity, con el detalle de cada error
Para quiénDefensa contra atacantesCorrección de datos de usuarios legítimos

La idea es descargar la validación de entrada del backend. En lugar de que cada servicio repita las mismas comprobaciones, las definís una vez en el borde y obtenés respuestas 422 consistentes en todos los sitios de tu cuenta.

Cuando un dato no cumple el esquema, Tero Services responde con 422 Unprocessable Entity y un cuerpo application/problem+json (RFC 7807) que enumera cada problema, con la misma forma que los issues de Zod:

{
"type": "https://tero.proxy/errors/validation",
"title": "Validation failed",
"status": 422,
"errors": [
{ "path": ["age"], "code": "too_small", "message": "Number must be >= 18" },
{ "path": ["email"], "code": "invalid_string", "message": "Invalid email" }
]
}

Cada error trae su ruta al campo (path), un código estable y legible por máquina (invalid_type, too_small, invalid_string, unrecognized_keys, entre otros) y un mensaje para humanos. Tu front puede mapear estos errores a los campos del formulario directamente. El código de estado es configurable por regla (422 por defecto).

El esquema se escribe como un documento JSON que refleja la API de Zod. Si tu front ya usa Zod, podés exportar el esquema o reescribirlo casi 1:1. Por ejemplo:

// El equivalente Zod del esquema de abajo:
z.object({
email: z.string().email(),
age: z.number().int().gte(18),
role: z.enum(['admin', 'user']),
nick: z.string().min(3).max(20).optional(),
}).strict()
{
"type": "object",
"strict": true,
"fields": {
"email": { "type": "string", "checks": [{ "email": true }] },
"age": { "type": "number", "checks": [{ "int": true }, { "gte": 18 }] },
"role": { "type": "enum", "values": ["admin", "user"] },
"nick": { "type": "string", "optional": true, "checks": [{ "min": 3 }, { "max": 20 }] }
}
}
TipoComprobaciones disponibles
stringmin, max, length, email, url, uuid, datetime (ISO-8601), regex, startsWith, endsWith
numberint, gte, lte, gt, lt, positive, negative, multipleOf
boolean(sin comprobaciones adicionales)
enumlista de values permitidos
arraymin, max, length, items (esquema de cada elemento)
objectfields (anidables), strict (rechaza claves no declaradas)

Cualquier campo admite además optional (la clave puede faltar) y nullable (el valor puede ser null), igual que en Zod.

En una query string o en un formulario application/x-www-form-urlencoded todo llega como texto: age=25 es el string "25", no el número 25. Para esos casos agregá la comprobación coerce (equivalente a z.coerce.number()), que convierte el valor antes de validarlo:

{ "type": "number", "checks": [{ "coerce": true }, { "gte": 18 }] }

Sin coerce, los valores de texto se validan como texto.

Una regla de validación apunta a una parte de la solicitud:

  • query: los parámetros de la query string. Se valida en cualquier método y no requiere leer el cuerpo, por lo que es la opción más barata.
  • body: el cuerpo de la solicitud (POST, PUT, PATCH). Se admite application/json y application/x-www-form-urlencoded.
  • both: ambos.

La validación de cuerpo funciona en todos los tipos de ruta (Proxy inverso, PHP y Lambda). Para no romper el streaming del proxy inverso, el cuerpo solo se almacena temporalmente cuando una regla de cuerpo coincide con esa ruta; el resto del tráfico sigue fluyendo sin cambios. El tamaño máximo de cuerpo a validar es de 256 KiB por defecto; un cuerpo mayor se rechaza con 413 Payload Too Large.

Las reglas se definen por cuenta y se aplican con un alcance de granularidad creciente:

AlcanceAplica a
GlobalTodos los dominios y paths de tu cuenta.
DominioTodo un dominio (por ejemplo, api.miempresa.com).
PathUn patrón de path dentro de un dominio (por ejemplo, /api/users/**).

En los patrones de path, * coincide con un segmento y ** con el resto de la ruta. Cada regla puede además acotarse a ciertos métodos HTTP (POST, PUT, etc.).

Cuando varias reglas podrían aplicar a una misma solicitud, gana la más específica: un patrón de path tiene prioridad sobre una regla de dominio, y esta sobre una regla global. La resolución es determinista, sin mezclar reglas.

Igual que el WAF, cada regla puede correr en uno de dos modos:

  • Bloqueo (por defecto): la solicitud que no valida se rechaza con el estado configurado.
  • Solo registro: la solicitud se deja pasar, pero el fallo se cuenta y se registra. Sirve para medir el impacto de una regla nueva antes de activar el bloqueo real, sin riesgo para el tráfico en producción.

Definís tus esquemas y reglas y se los entregás al equipo de Host Admin (la autogestión vía panel está en la hoja de ruta). Antes de guardarse, cada esquema se compila y se verifica: una expresión regular mal formada o un tipo desconocido se rechazan en el momento del alta, no en producción. Los cambios se aplican sin reiniciar el servicio.

  • Métrica Prometheus tero_form_validation_total{outcome}, con outcome en pass, fail, monitor o skip.
  • Cada bloqueo deja una entrada en el plano de auditoría de datos (método, path, dirección IP, regla aplicada y los primeros errores), sin registrar el contenido sensible del cuerpo.
  • Las altas y modificaciones de reglas quedan en el Registro de auditoría.
  • Se admiten application/json y application/x-www-form-urlencoded. Los cuerpos multipart/form-data (subida de archivos) quedan fuera del alcance actual.
  • La validación de cuerpo está acotada a 256 KiB por solicitud (configurable); cuerpos mayores se rechazan con 413.
  • No reemplaza la validación de tu aplicación: es una capa de defensa y consistencia adicional en el borde, no la única.
  • ¿Puedo reutilizar mis esquemas de Zod del front? Sí. El formato es un espejo declarativo de Zod; en la mayoría de los casos la traducción es directa.
  • ¿Afecta el rendimiento de quien no la usa? No. Si tu cuenta no define reglas, la validación se omite por completo en el camino de la solicitud.
  • ¿Qué diferencia hay con el WAF? El WAF detecta ataques y responde 403 opaco; la validación comprueba datos de negocio y responde 422 con el detalle. Conviene usarlos juntos.