Zum Inhalt springen

Autorisierung ist nicht Authentifizierung

Laravels Auth-Scaffolding beantwortet, wer Sie sind. Wer was darf, schreiben Sie - und die Tests, die es beweisen, sind die, die niemand schreibt: die, die eine Ablehnung behaupten.

5 Min. Lesezeit

Laravel macht Sie an einem Nachmittag authentifiziert. Registrierung, Login, Passwort-Zurücksetzung, Tokens, Zwei-Faktor auf Wunsch - installiert, getestet, konventionell.

Dann muss die Anwendung entscheiden, was jede authentifizierte Person darf, und das Framework lehnt es zu Recht ab, das zu raten. Dieser Teil gehört Ihnen, und dort liegen die Sicherheitsfehler, die wir in Audits tatsächlich finden.

Der Unterschied, klar gesagt

Authentifizierung stellt fest, wer die Anfrage stellt. Das ist ein gelöstes Problem, und Sie sollten es nicht selbst lösen.

Autorisierung entscheidet, ob diese Person diese Handlung an diesem Objekt vornehmen darf. Das ist Domänenlogik. Niemand kann sie für Sie ausliefern, denn sie ist eine Aussage über Ihr Geschäft.

Fast jeder ernsthafte Zugriffsfehler, den wir in einer Laravel-Anwendung gefunden haben, war ein Autorisierungsfehler auf einer Route, auf der Authentifizierung einwandfrei funktionierte.

Der Fehler in seiner häufigsten Form

public function show(Invoice $invoice)
{
    return view('invoices.show', compact('invoice'));
}

Die Route liegt hinter der auth-Middleware. Der Nutzer ist angemeldet. Route Model Binding hat die ID in eine Rechnung aufgelöst.

Nichts hat gefragt, ob es seine Rechnung ist. Ändern Sie die Nummer in der URL, und Sie haben die von jemand anderem. Auf einer mandantenfähigen Plattform ist das ein mandantenübergreifendes Datenleck, und die Route sieht im Review völlig normal aus.

Die Behebung ist eine Zeile, und die Disziplin ist, jedes Mal daran zu denken:

public function show(Invoice $invoice)
{
    $this->authorize('view', $invoice);
 
    return view('invoices.show', compact('invoice'));
}

Eine Regel, ein Ort, jeder Einstiegspunkt

Der zweite Fehler ist Duplikation. Eine Berechtigung wird im Controller für die Web-Routen umgesetzt, dann in einer Middleware für die API erneut, dann ungefähr noch einmal in einer Blade-Bedingung, die den Knopf versteckt.

Drei Kopien einer Regel driften. Eine wird falsch, und es ist meist die, auf die niemand schaut.

Die Regel gehört in eine Policy, und jeder Einstiegspunkt ruft sie auf:

class InvoicePolicy
{
    public function view(User $user, Invoice $invoice): bool
    {
        return $user->team_id === $invoice->team_id;
    }
}

Controller rufen sie auf. API-Ressourcen rufen sie auf. Blade fragt @can('view', $invoice), um zu entscheiden, ob der Knopf gerendert wird - und den Knopf zu verstecken ist Darstellung, niemals Durchsetzung. Ein versteckter Knopf ist immer noch eine Route.

Jobs und Konsolenbefehle brauchen denselben Gedanken. Ein eingereihter Export, der einen Bericht "für einen Nutzer" baut, ohne den Geltungsbereich erneut zu prüfen, ist eine Art, eine Autorisierungsprüfung aus dem System zu waschen.

Eigentum schlägt Rollen

Rollenprüfungen beantworten, welche Art Nutzer das ist. Sie gehen fröhlich durch, während der Nutzer fremde Daten anfasst.

// geht für jeden Admin durch, auch den eines anderen Mandanten
if ($user->hasRole('admin')) { ... }
 
// stellt die Frage, auf die es ankommt
return $user->team_id === $invoice->team_id
    && $user->hasRole('admin');

Wenden Sie in einem mandantenfähigen System die Mandantenbedingung global an, statt sie je Abfrage zu erinnern - ein globaler Scope, ein Scoped Binding oder eine Repository-Schicht, die sich nicht versehentlich umgehen lässt. Die Regel lautet: Vergessen soll keine Zeilen liefern, nicht die Zeilen eines anderen Mandanten.

Die Ablehnung testen, nicht die Erlaubnis

Das ist die praktische Erkenntnis.

it('verweigert eine Rechnung eines anderen Teams', function () {
    $invoice = Invoice::factory()->create();          // irgendein anderes Team
    $intruder = User::factory()->create();
 
    $this->actingAs($intruder)
        ->get("/invoices/{$invoice->id}")
        ->assertForbidden();
});

Tests für den glücklichen Pfad entstehen von selbst, weil das die gebaute Funktion ist. Der negative Fall ist der, der die Regression fängt, und eine Testsuite ohne assertForbidden hat Autorisierung nie geprüft.

Schreiben Sie einen je Ressource, für den falschen Nutzer, den falschen Mandanten und die nicht authentifizierte Anfrage. Es ist ein kurzer Nachmittag und das wertvollste Testen, das Sie in einer Geschäftsanwendung tun können.

Wonach wir in einem Audit suchen

Jede Route ohne Autorisierungsaufruf. Jede Policy-Methode, die bedingungslos true liefert. Massenzuweisbare Felder, die über Rechte entscheiden - ein role oder team_id in $fillable ist eine Rechteausweitung, die auf ein Formular-POST wartet. Jede Stelle, an der eine Abfrage nicht auf den aktuellen Mandanten eingeschränkt ist. Und die API, immer, denn dort wurden die Regeln der Web-Routen von jemandem in Eile nachgebaut.

Nichts davon ist exotisch. Es ist dieselbe kleine Menge an Auslassungen, und sie sind mit einer Checkliste günstiger zu finden als mit einer Meldung.

Ein Audit geht diese Checkliste durch und hält fest, was es nicht gefunden hat. Diese Notizen sind die Tests, die niemand geschrieben hatte. Gilt der Zweifel speziell der API, beginnen Sie eine Ebene höher: Sanctum und Passport ziehen die Autorisierungsgrenze an unterschiedlichen Stellen, und welches Paket Sie einsetzen, entscheidet, wie viel davon Sie selbst bauen.

Verwandte Fragen

Wo gehört Autorisierung hin?
In Policies, aufgerufen von jedem Einstiegspunkt. Der Fehler, den wir am häufigsten finden, ist eine Regel, die im Controller für das Web und noch einmal in einer Middleware für die API umgesetzt ist und auseinanderdriftet, bis eine von beiden falsch ist - meist die API, weil niemand hinsieht.
Reicht eine Rollenprüfung?
Rollen beantworten, welche Art Nutzer das ist, nicht ob dieser Nutzer diesen Datensatz anfassen darf. Die meisten echten Vorfälle in Geschäftsanwendungen sind ein gültiger Nutzer, der die Zeile eines anderen Mandanten erreicht, und eine Rollenprüfung lässt das jedes Mal durch. Die Eigentumsprüfung ist die, auf die es ankommt.
Wie sieht ein guter Autorisierungstest aus?
Er behauptet eine Ablehnung. Tests entstehen meist aus dem glücklichen Pfad - die Eigentümerin kann ihre eigene Rechnung öffnen - und der Fall, auf den es ankommt, ist, dass jemand anderes es nicht kann. Eine Testsuite ohne 403-Behauptungen testet Autorisierung überhaupt nicht.
Erledigt Route Model Binding das?
Nein, und das gehört ausdrücklich gesagt. Binding löst die ID in ein Modell auf; es fragt nicht, ob der aktuelle Nutzer es haben darf. Scoped Bindings beschränken die Suche auf eine übergeordnete Beziehung, was sehr hilft und trotzdem kein Ersatz für eine Policy ist.

← Zurück zu allen Artikeln

Anrufen+1 848 272 7583WhatsApp+90 850 308 5436E-Mailinfo@codefacture.comKontaktseite