Ir al contenido

Protección de Formularios Públicos

Cualquier endpoint que acepte entrada de usuarios no autenticados (o en proceso de autenticación) enfrenta riesgos automatizados. El patrón de protección debe mitigar:

  • Credential Stuffing / Account Takeover (ATO): Uso masivo de credenciales filtradas para intentar acceder a cuentas legítimas en la pantalla de Login.
  • Carding / Fraude de Pagos: Bots probando lotes de tarjetas de crédito robadas en la pantalla de Checkout.
  • Scraping / Enumeración de Datos: Extracción automatizada de catálogos, precios o inventario.
  • DDoS de Capa 7 (L7): Sobrecarga del backend enviando miles de peticiones aparentemente válidas a los formularios.
  • Form Spam / Creación de Cuentas Falsas: Inundación de las pantallas de Registro y Contacto con basura.

En Grupo Herdez, reCAPTCHA Enterprise es el único estándar aprobado para la protección contra bots. Queda prohibido el uso de reCAPTCHA v2 (casillas de “No soy un robot”) y v3 estándar.

  • Endpoints de APIs B2B internas: Si la comunicación es de backend a backend (server-to-server), usa autenticación de servicio (Workload Identity, mTLS, JWT). reCAPTCHA requiere telemetría del navegador.
  • Formularios en intranets cerradas: Aplicaciones que solo están disponibles dentro de la VPN corporativa y no enfrentan amenazas de bots masivos de internet.

La seguridad de reCAPTCHA depende de validar el token en el backend. Un token solo generado en el cliente no provee protección.

Flujo de Validación — reCAPTCHA Enterprise + Cloud Armor
Arquitectura de defensa en profundidad para formularios públicos.
1. Cliente (Navegador/App)
Carga el script, ejecuta el desafío invisible y obtiene un token encriptado.
grecaptcha.enterprise.execute()
↓ Token enviado en el payload del POST
2. Cloud Armor (Perímetro)
Aplica rate limiting por IP, bloquea geolocalizaciones no permitidas y filtra SQLi/XSS.
↓ Tráfico limpio llega al backend
3. Backend (GKE / Cloud Run)
Recibe el token y lo envía a la API de reCAPTCHA para evaluación antes de procesar el request.
POST /v1/projects/.../assessments
↓ Solicita assessment usando Workload Identity
4. reCAPTCHA Enterprise API
Analiza el token y devuelve un *score* (0.0 a 1.0) y razones de riesgo.
↑ Retorna { valid: true, score: 0.9, action: "login" }
5. Decisión de Negocio (Backend)
Aplica la matriz de umbrales según la acción solicitada.
Score > 0.7: Permitir 0.3 - 0.7: Step-Up (MFA) Score < 0.3: Rechazar
  1. Frontend: Genera un token usando la clave de sitio (Site Key) de reCAPTCHA al momento de interactuar con el formulario.
  2. Backend: Recibe el token en el payload del request, llama a la API de GCP para obtener el assessment y decide la acción.

Al recibir el assessment de reCAPTCHA, el backend debe realizar obligatoriamente estas tres validaciones en orden:

  1. Token Válido (valid): ¿El token fue emitido por Google, no ha expirado y no ha sido usado antes?
  2. Acción Coincidente (action): ¿El token fue generado para la acción correcta (ej. login vs checkout)? Esto previene ataques de repetición.
  3. Umbral de Puntuación (score): ¿El riesgo es aceptable para el negocio?

No existe un score “mágico” para todo. Las aplicaciones deben implementar una decisión en tres niveles:

Nivel de ScoreClasificaciónAcción del Backend
0.7 a 1.0Tráfico legítimo / HumanoPermitir (Allow): Procesar el request normalmente.
0.3 a 0.6Tráfico sospechosoStep-Up / MFA: Requerir fricción adicional (ej. enviar código por SMS, forzar login).
0.0 a 0.2Bot confirmado / MaliciosoRechazar (Deny): Bloquear silenciosamente o retornar error genérico HTTP 403.

Nota: Estos valores son una calibración inicial recomendada. Los umbrales definitivos deben ajustarse analizando la telemetría real en producción.


Implementación: Ejemplo End-to-End Productizable

Sección titulada «Implementación: Ejemplo End-to-End Productizable»

Antes de que el tráfico llegue al backend, Cloud Armor descarta el ruido en el edge de GCP. La política estándar para formularios públicos combina rate limiting por IP + WAF preconfigurado de Google (SQLi / XSS) + default allow.

Política estándar: hera-public-forms-policy

Sección titulada «Política estándar: hera-public-forms-policy»
ReglaPrioridadAcciónCondición de matchPropósito
Rate limit por IP1000rate_based_bandeny(403) al excederTodos los orígenes (*)Mitigar DDoS de capa 7 limitando a 100 req/min por IP
WAF preconfigurado2000deny(403)sqli-v33-stable OR xss-v33-stableBloquear patrones conocidos de inyección SQL y XSS
Default2147483647allowTodos los orígenesPermitir el resto del tráfico legítimo
ParámetroValor estándarJustificación
conform_actionallowTráfico dentro del umbral pasa sin fricción
exceed_actiondeny(403)Respuesta genérica sin revelar detalle al atacante
rate_limit_threshold.count100Cota razonable para un formulario público humano
rate_limit_threshold.interval_sec60Ventana de 1 minuto (estándar para rate limits de capa 7)
enforce_on_keyIPCada IP es un bucket independiente
  • 100 req/min por IP: un humano difícilmente supera este umbral en un formulario; los bots sí. Ajustable si el formulario tiene uso corporativo legítimo detrás de un NAT compartido (ver siguiente sección).
  • WAF preconfigurado (v33-stable): Google mantiene las firmas actualizadas; usar reglas custom requiere operación continua que no aporta valor en formularios públicos estándar.
  • Default allow: Cloud Armor actúa como filtro, no como gate. El control de acceso real lo hace la aplicación (reCAPTCHA + sesión).

2. Integración en Frontend (Next.js / React)

Sección titulada «2. Integración en Frontend (Next.js / React)»
import { useEffect, useCallback } from 'react';
// # simplificado para ilustración - Reemplazar <SITE_KEY> por variable de entorno
const SITE_KEY = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;
export default function LoginForm() {
const handleLogin = useCallback(async (event) => {
event.preventDefault();
// Obtener el token de reCAPTCHA
const token = await window.grecaptcha.enterprise.execute(SITE_KEY, {
action: 'login',
});
// Enviar credentials + token al backend
await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com',
password: '<TU_PASSWORD_AQUI>', // # simplificado para ilustración, usar refs o state
recaptchaToken: token,
}),
});
}, []);
return (
<form onSubmit={handleLogin}>
{/* ... campos del formulario ... */}
<button type="submit">Iniciar Sesión</button>
</form>
);
}

Este ejemplo es productizable y utiliza el SDK oficial autenticado automáticamente con ADC (Application Default Credentials).

const { RecaptchaEnterpriseServiceClient } = require('@google-cloud/recaptcha-enterprise');
// Instancia global utilizando Workload Identity de GKE
const recaptchaClient = new RecaptchaEnterpriseServiceClient();
async function createAssessment(token, recaptchaAction, projectID, siteKey) {
const projectPath = recaptchaClient.projectPath(projectID);
const request = {
parent: projectPath,
assessment: {
event: {
token: token,
siteKey: siteKey,
expectedAction: recaptchaAction,
},
},
};
try {
const [response] = await recaptchaClient.createAssessment(request);
// 1. Validar integridad del token
if (!response.tokenProperties.valid) {
console.error(`Token inválido: ${response.tokenProperties.invalidReason}`);
return { allow: false, reason: 'INVALID_TOKEN' };
}
// 2. Validar que la acción coincida
if (response.tokenProperties.action !== recaptchaAction) {
console.error('La acción del token no coincide');
return { allow: false, reason: 'ACTION_MISMATCH' };
}
// 3. Validar el Risk Score
const score = response.riskAnalysis.score;
if (score >= 0.7) {
return { allow: true, score };
} else if (score >= 0.3) {
// Step-Up (ej. solicitar MFA)
return { allow: false, stepUp: true, score };
} else {
// Rechazar (Bot)
return { allow: false, reason: 'BOT_DETECTED', score };
}
} catch (error) {
console.error('Error invocando API de reCAPTCHA', error);
// Fail-open o fail-closed depende de la criticidad del negocio
// Para eCommerce: mejor fail-open (permitir compra, alertar) que bloquear ventas por falla de red
return { allow: true, reason: 'API_ERROR_FALLBACK' };
}
}

Los umbrales y acciones no son iguales para todos los formularios. Cada tipo de endpoint tiene un perfil de riesgo distinto:

FormularioAcción reCAPTCHAUmbral AllowUmbral Step-UpUmbral DenyRate Limit (Cloud Armor)Rate Limit (App)
Loginlogin≥ 0.70.3 – 0.6 (→ MFA)< 0.3100/min/IP5 intentos/email/15 min
Registrosignup≥ 0.70.4 – 0.6 (→ verificación email)< 0.450/min/IP3 cuentas/IP/hora
Checkout / Pagoscheckout≥ 0.80.5 – 0.7 (→ re-auth)< 0.530/min/IP10 transacciones/usuario/hora
Contacto / Soportecontact≥ 0.50.2 – 0.4 (→ honeypot)< 0.220/min/IP5 mensajes/email/hora
Recuperación de contraseñapassword_reset≥ 0.70.3 – 0.6 (→ delay progresivo)< 0.330/min/IP3 solicitudes/email/hora

reCAPTCHA no es una bala de plata. El sistema debe complementarse con:

  1. Rate Limiting por Cuenta: Cloud Armor limita por IP, pero un ataque distribuido usará miles de IPs. El backend debe limitar intentos por nombre de usuario (user@email.com).
  2. Validación de Input Estricta: Las bibliotecas como Zod o Joi deben sanitizar todo el input antes de que llegue a la lógica de negocio.
  3. Auditoría: Cada login fallido o evaluación rechazada debe dejar un trazo de log estructurado para análisis de SOC.

Antes de aprobar un Merge Request que exponga un formulario público, el Tech Lead debe validar:

  • Se implementa reCAPTCHA Enterprise, no v2 ni v3 estándar.
  • El backend valida el token (integridad valid).
  • El backend valida explícitamente que el action enviado coincide con el esperado.
  • Existe una lógica de evaluación basada en el score (tri-tier u otra calibrada).
  • La autenticación hacia la API de reCAPTCHA utiliza Workload Identity (sin claves JSON).
  • La ruta está protegida detrás de una política WAF de Cloud Armor con rate limiting por IP.
  • Existe rate limiting a nivel de aplicación por entidad de negocio (ej. por email).