Ir al contenido

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.

5 min de lectura

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.

Preguntas relacionadas

¿Dónde debería vivir la autorización?
En policies, llamadas desde todos los puntos de entrada. El fallo que más encontramos es una regla implementada en un controlador para la web y otra vez en un middleware para la API, separándose hasta que una de las dos está mal, y suele ser la de la API porque nadie la mira.
¿Basta con comprobar el rol?
Los roles responden qué tipo de usuario es este, no si este usuario puede tocar este registro. La mayoría de las brechas reales en aplicaciones de negocio son un usuario válido alcanzando la fila de otro inquilino, y una comprobación de rol deja pasar eso siempre. La comprobación de propiedad es la que importa.
¿Cómo es una buena prueba de autorización?
Afirma un rechazo. Las pruebas se escriben normalmente desde el camino feliz - la propietaria puede abrir su propia factura - y el caso que importa es que otra persona no pueda. Una batería sin afirmaciones de 403 no está probando la autorización en absoluto.
¿El route model binding se encarga de esto?
No, y conviene decirlo explícitamente. El binding resuelve el id en un modelo; no pregunta si el usuario actual puede tenerlo. Los bindings con ámbito restringen la búsqueda a una relación padre, lo cual ayuda bastante y sigue sin sustituir a una policy.

← Volver a todos los artículos

Llamar+1 848 272 7583WhatsApp+90 850 308 5436Correoinfo@codefacture.comPágina de contacto