La autorización no es la autenticación
El andamiaje de auth de Laravel responde quién es usted. Quién puede hacer qué lo escribe usted, y las pruebas que lo demuestran son las que nadie escribe - las que afirman un rechazo.
Laravel le deja autenticado en una tarde. Registro, login, restablecimiento de contraseña, tokens, doble factor si lo quiere: instalado, probado y convencional.
Después la aplicación tiene que decidir qué puede hacer cada persona autenticada, y el framework se niega, con razón, a adivinarlo. Esa parte es suya, y ahí es donde viven de verdad los fallos de seguridad que encontramos en las auditorías.
La distinción, dicha sin rodeos
La autenticación establece quién está haciendo la petición. Es un problema resuelto y no debería estar resolviéndolo usted.
La autorización decide si esa persona puede realizar esta acción sobre este objeto. Es lógica de dominio. Nadie puede entregársela hecha, porque es una afirmación sobre su negocio.
Casi todos los fallos serios de control de acceso que hemos encontrado en una aplicación Laravel eran fallos de autorización en una ruta donde la autenticación funcionaba perfectamente.
El fallo, en su forma más común
public function show(Invoice $invoice)
{
return view('invoices.show', compact('invoice'));
}La ruta está detrás del middleware auth. El usuario ha iniciado sesión. El
route model binding resolvió el id en una factura.
Nada preguntó si es su factura. Cambie el número de la URL y tiene la de otra persona. En una plataforma multiinquilino eso es una fuga de datos entre inquilinos, y la ruta parece completamente normal en la revisión.
El arreglo es una línea y la disciplina es acordarse cada vez:
public function show(Invoice $invoice)
{
$this->authorize('view', $invoice);
return view('invoices.show', compact('invoice'));
}Una regla, un sitio, todos los puntos de entrada
El segundo fallo es la duplicación. Un permiso se implementa en un controlador para las rutas web, luego se vuelve a implementar en un middleware para la API, y luego aproximadamente otra vez en una condición de Blade que esconde el botón.
Tres copias de una regla se separan. Una se vuelve incorrecta, y suele ser la que nadie mira.
La regla va en una policy, y todos los puntos de entrada la llaman:
class InvoicePolicy
{
public function view(User $user, Invoice $invoice): bool
{
return $user->team_id === $invoice->team_id;
}
}La llaman los controladores. La llaman los recursos de API. Blade pregunta
@can('view', $invoice) para decidir si renderiza el botón, y esconder el
botón es presentación, nunca aplicación de la regla. Un botón escondido sigue
siendo una ruta.
Los trabajos y los comandos de consola también necesitan esta reflexión. Una exportación en cola que construye un informe "para un usuario" sin volver a comprobar el ámbito es una manera de lavar una comprobación de autorización fuera del sistema.
La propiedad le gana a los roles
Las comprobaciones de rol responden qué tipo de usuario es este. Pasan tan tranquilas mientras el usuario toca los datos de otro.
// pasa para cualquier admin, incluido el de otro inquilino
if ($user->hasRole('admin')) { ... }
// hace la pregunta que importa
return $user->team_id === $invoice->team_id
&& $user->hasRole('admin');En un sistema multiinquilino, aplique la restricción de inquilino de forma global en lugar de recordarla por consulta: un scope global, un binding con ámbito, o una capa de repositorio que no se pueda saltar por accidente. La regla es que olvidarse produzca cero filas, no las filas de otro inquilino.
Pruebe el rechazo, no el permiso
Esta es la conclusión práctica.
it('rechaza una factura de otro equipo', function () {
$invoice = Invoice::factory()->create(); // otro equipo cualquiera
$intruder = User::factory()->create();
$this->actingAs($intruder)
->get("/invoices/{$invoice->id}")
->assertForbidden();
});Las pruebas del camino feliz se escriben por defecto porque esa es la
funcionalidad que se está construyendo. El caso negativo es el que caza la
regresión, y una batería sin ningún assertForbidden es una batería que nunca
ha comprobado la autorización.
Escriba una por recurso, para el usuario equivocado, el inquilino equivocado y la petición sin autenticar. Es una tarde corta y es el trabajo de pruebas de mayor valor que puede hacer en una aplicación de negocio.
Qué buscamos en una auditoría
Cada ruta sin llamada de autorización. Cada método de policy que devuelve true
sin condición. Campos asignables en masa que deciden permisos: un role o un
team_id en $fillable es una escalada esperando a un envío de formulario.
Cualquier sitio donde una consulta no esté restringida al inquilino actual. Y
la API, siempre, porque es donde alguien con prisa volvió a implementar las
reglas de las rutas web.
Nada de esto es exótico. Es el mismo pequeño conjunto de omisiones, y salen más baratas con una lista de comprobación que con una notificación de incidente.
Una auditoría recorre esa lista y anota lo que no encontró. Esas notas son las pruebas que nadie había escrito. Si la duda es concretamente sobre la API, empiece un nivel más arriba: Sanctum y Passport trazan la línea de autorización en sitios distintos, y el que use decide cuánto de esto construye usted.
