No construyas un oráculo
Cuando se envía un código para la dirección equivocada, el rechazo es cuidadoso con lo que dice:
if ($payloadEmail !== $email) {
Cache::put($attemptsKey, $attempts + 1, now()->addMinutes(15));
// Deliberately does not echo the address the code belongs to — that
// would turn a lucky guess into an account-discovery oracle.
return Response::json([
'verified' => false,
'error' => 'Email mismatch',
'reason' => 'The verification code was sent to a different email address',
'provided_email' => $email,
// …
]);
}
La versión servicial —«ese código pertenece a j**@ejemplo.com»*— convierte un acierto por suerte en una forma de descubrir quién se está registrando. La respuesta sólo devuelve la dirección que el llamante ya aportó.
Fíjate en que el desajuste sigue costando un intento. Un rechazo gratis no es un límite.
Hay un matiz que merece honestidad, porque esta herramienta sí distingue otros dos casos:
$wasSent = Cache::has("verify-code-sent:{$code}");
Caducado e inválido dan mensajes distintos, lo cual es técnicamente divulgación de información: confirma que un código existió alguna vez. Es un intercambio deliberado: sin eso, a un usuario cuyo código expiró se le dice que su código correcto está mal, y llegan tickets de soporte. Lo que lo hace aceptable es que la distinción no revela nada sobre de quién era el código, y el presupuesto de intentos sigue aplicándose.
Pregúntale a cada mensaje de error: ¿qué le confirma esto a alguien que está adivinando? Y luego decide, en vez de tirar por defecto hacia lo servicial.
Las vinculaciones hechas al emitir son las autoritativas
Esta es la más sutil, y la más fácil de equivocar escribiendo código perfectamente razonable.
Cuando se envió el código, quedó vinculado a un contacto. La herramienta también acepta un argumento lead_id, porque el modelo tiene uno en contexto. ¿Cuál gana?
// The code->lead binding set when the code was sent is authoritative. Only
// fall back to a caller-supplied lead_id when that binding is gone, and even
// then only if the lead actually belongs to the just-verified email —
// otherwise a caller could attach their verified email to someone else's
// lead and thread it into the site creation.
$leadId = Cache::pull("verify-code-lead-id:{$code}");
Fiarse del argumento significa: verifica una dirección que controlas, pasa el identificador de contacto de otra persona, y ahora tu correo verificado está adjunto al registro de esa persona —que luego fluye hacia la creación. Cada paso individual pasa su propia validación. El agujero está en la composición.
Un valor establecido en el momento de emitir gana a un valor aportado en el momento de usar, siempre. Y donde una alternativa sea inevitable, hay que revalidarla contra algo ya demostrado —aquí, que el contacto realmente pertenece a la dirección recién verificada.
Busca esta forma en tus propias herramientas: un argumento que duplica algo que el servidor ya sabe. O gana la copia del servidor, o tienes un parámetro que permite a un llamante reapuntar la operación.