Ir al contenido principal
Laravel, thinking fast.

Capitulo 11

El modelo es un llamante en el que no se confia

Julian Beaujardin

El modelo es complaciente. Eso es lo que lo hace útil, y es toda la superficie de ataque.

Quiere completar la tarea. Si le das una regla, la sigue —hasta que alguien construye una conversación en la que romperla parece seguir una regla mejor. No tiene concepto de tu modelo de amenazas, ni memoria del llamante anterior, ni forma de distinguir a un cliente de alguien que te está sondeando.

Así que el cuarto principio de este libro, dicho sin rodeos: cualquier cosa que impongas pidiéndosela amablemente al modelo no está impuesta. Este capítulo es el conjunto de cosas que deben ir detrás de las herramientas en su lugar.

Cuenta los intentos contra la identidad, no contra el intento

Un código de seis dígitos tiene un millón de posibilidades, así que necesita un límite de intentos. Dónde guardas ese contador es toda su seguridad:

// Attempts are counted per email address, never per submitted code —
// keying on the code would hand every new guess its own fresh budget.
$attemptsKey = "verify-attempts:{$email}";

Si lo indexas por el código enviado, cada intento fallido es su propio primer intento, para siempre. El límite existe, el código lo aplica correctamente y no detiene nada.

Indéxalo por la cosa que se protege —la dirección de correo— y tres fallos son tres fallos, lleguen como lleguen.

Cuenta contra el recurso, no contra la petición. El mismo error aparece con límites de frecuencia indexados por un valor que controla el llamante.

Dos ventanas, o has moderado el abuso en vez de detenerlo

private const PER_MINUTE_LIMIT = 3;

/**
 * A per-minute ceiling alone only paces abuse, it never stops it: three
 * codes a minute, forever, is thousands of mails a day to an address
 * nobody has proven they own. The daily cap is what bounds the spend and
 * keeps the sending domain off spam lists.
 */
private const PER_DAY_LIMIT = 10;

Un límite por minuto parece protección porque detiene la ráfaga evidente. Haz la cuenta y permite miles de correos al día a una dirección que nadie ha demostrado poseer —lo que es una herramienta de bombardeo de correo con tu dominio encima, y un problema de reputación que sobrevive al incidente.

Es la distinción del capítulo 4 entre límite de frecuencia y presupuesto, en un contexto de seguridad. La ventana corta modela el tráfico; la larga acota el daño total. Necesitas las dos, y la larga es la que importa.

Y ambas se incrementan con el mismo cuidado:

// add() then increment(), rather than read-modify-write, so two concurrent
// sends cannot both read the same count and write it back — which would let
// a cap be overrun a little at a time. Both windows need this: N simultaneous
// requests all reading 0 defeats the per-minute ceiling just as readily as
// the daily one.
private static function bump(string $key, DateTimeInterface|int $ttl): void
{
    Cache::add($key, 0, $ttl);
    Cache::increment($key);
}

La misma lección que con el techo de gasto: comprobar-y-luego-escribir falla abriendo bajo concurrencia, y un límite de seguridad nunca debe fallar abriendo.