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.
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.
