10 häufige Sicherheitsfehler in Laravel-Projekten

Zehn typische Entwicklungsfehler, durch die Zugriffskontrollen, Uploads, Datenbankabfragen, Webhooks und Produktionssysteme angreifbar werden.

Laravel bringt sichere Bausteine für Authentifizierung, Autorisierung, CSRF-Schutz, Datenbankzugriff und Dateispeicherung mit. Das macht eine Anwendung aber nicht automatisch sicher. Die meisten kritischen Schwachstellen entstehen dort, wo Entwickler eine fachliche Vertrauensgrenze falsch modellieren, eine bequeme Abkürzung nehmen oder nur den erfolgreichen Ablauf testen.

Das Framework schützt viele technische Standardsituationen. Wer auf welche Daten und Aktionen zugreifen darf, bleibt eine Entscheidung der Anwendung.

Die folgenden zehn Fehler sehe ich als besonders relevant an. Sie sind nicht auf alten Laravel-Versionen beschränkt. Auch eine aktuelle Anwendung kann verwundbar sein, wenn ihre Sicherheitsannahmen nicht explizit umgesetzt und negativ getestet werden.

Authentifizierung mit Autorisierung verwechseln

Ein angemeldeter Benutzer ist nicht automatisch berechtigt, jedes Objekt aufzurufen. Ein typisches unsicheres Muster lädt eine Rechnung, ein Ticket oder ein Projekt ausschließlich über die ID aus der URL. Wird 42 zu 43, erhält der Benutzer möglicherweise den Datensatz eines anderen Kunden. Einfaches Route Model Binding löst das Objekt auf, autorisiert den Zugriff aber nicht. Scoped Bindings können bei verschachtelten Ressourcen zusätzlich erzwingen, dass ein Kind zum angegebenen Elternobjekt gehört. Rollen, Aktionen und fachliche Zustände müssen trotzdem autorisiert werden.

Policies und Gates sollten die fachliche Aktion prüfen. Bei verschachtelten Ressourcen muss zusätzlich feststehen, dass Kind und Elternobjekt zusammengehören. Ein sicherer Test prüft nicht nur, dass der Eigentümer Zugriff erhält, sondern auch, dass ein zweiter Benutzer trotz gültiger Anmeldung 403 oder 404 erhält.

Unsicher ist eine Aktion, die nur auf eine gültige ID vertraut:

public function show(Invoice $invoice): InvoiceResource
{
    return new InvoiceResource($invoice);
}

Die Policy wird explizit ausgeführt und enthält die fachliche Grenze:

public function show(Invoice $invoice): InvoiceResource
{
    Gate::authorize('view', $invoice);

    return new InvoiceResource($invoice);
}

public function view(User $user, Invoice $invoice): bool
{
    return $user->tenant_id === $invoice->tenant_id
        && ($user->id === $invoice->owner_id || $user->can('invoice.read.all'));
}

Der wichtigste Test ist der abgelehnte Zugriff:

it('denies an invoice from another tenant', function () {
    $tenant = Tenant::factory()->create();
    $foreignTenant = Tenant::factory()->create();
    $user = User::factory()->for($tenant)->create();
    $foreignInvoice = Invoice::factory()->for($foreignTenant)->create();

    $this->actingAs($user)
        ->getJson(route('invoices.show', $foreignInvoice))
        ->assertForbidden();
});

Wichtige Prüffragen:

  • Wird jede lesende und schreibende Aktion autorisiert?
  • Umfasst die Policy Rolle, Mandant, Objekt und fachlichen Status?
  • Sind Downloads, Exporte und API-Endpunkte genauso geschützt wie HTML-Seiten?

Laravel 13 Authorization beschreibt Policies und Gates. Die fachliche Regel muss das Projekt trotzdem selbst definieren.

Empfehlung: Halten Sie die Autorisierungsentscheidung in einer Policy und schreiben Sie für jede sensible Objekt-Route einen Cross-User- oder Cross-Tenant-Test. Exporte und Downloads gehören ausdrücklich in diese Liste.

Mandantentrennung nur im Controller anwenden

Eine where('tenant_id', ...)-Bedingung in einigen Controllern reicht für eine mandantenfähige Plattform nicht aus. Daten verlassen das System auch über Jobs, Benachrichtigungen, Suchindizes, Exporte, Cache-Einträge, Broadcast-Kanäle und administrative Werkzeuge. Fehlt der Mandantenkontext an nur einer Stelle, können Datensätze zwischen Kunden sichtbar werden.

Tenant-Isolation sollte als systemweite Invariante behandelt werden. Datenmodell, Queries, Policies, Queue-Payloads, Cache-Keys und Dateipfade müssen dieselbe Grenze erzwingen. Globale Scopes können helfen, sind aber kein Ersatz für verständliche Policies und Tests. Ein bewusst deaktivierter Scope in einem Reporting-Job kann sonst die gesamte Sicherheitsannahme aufheben.

Negative Integrationstests sollten mindestens zwei Mandanten erzeugen und alle relevanten Zugriffswege gegeneinander prüfen. Besonders wertvoll sind Tests für Jobs und Exporte, weil dort kein aktueller Browser-Request mehr den Benutzerkontext liefert.

Ein Queue-Job sollte den Mandanten explizit transportieren und innerhalb dieses Kontexts erneut laden. Ein serialisiertes Eloquent-Modell allein macht die Grenze nicht sichtbar:

final class BuildProjectExport implements ShouldQueue
{
    public function __construct(
        public readonly int $tenantId,
        public readonly int $projectId,
        public readonly int $requestedBy,
    ) {}

    public function handle(): void
    {
        $tenant = Tenant::findOrFail($this->tenantId);
        $user = User::whereBelongsTo($tenant)->findOrFail($this->requestedBy);
        $project = Project::query()
            ->where('tenant_id', $this->tenantId)
            ->findOrFail($this->projectId);

        Gate::forUser($user)->authorize('export', $project);

        // Export auf einem tenant-spezifischen privaten Pfad erzeugen.
    }
}

Empfehlung: Definieren Sie eine zentrale Tenant-Auflösung, tenant-spezifische Cache- und Storage-Namespaces sowie eine Testmatrix für HTTP, CLI, Queue, Suche, Export und Broadcast. Eine Isolation, die nur im Controller existiert, ist keine Isolation.

Request-Daten ungefiltert an Eloquent übergeben

Mass Assignment wird häufig nur als Frage von $fillable betrachtet. Das eigentliche Risiko ist breiter: Ein Entwickler übergibt $request->all() oder ein zu großzügig validiertes Array direkt an create(), update() oder fill(). Wenn die $fillable- oder $guarded-Konfiguration diese Felder zulässt, können role, tenant_id, is_admin, status, price oder approved_at vom Client beeinflusst werden.

Auch $guarded = [] kann in internen Modellen vertretbar sein, verlangt aber eine sehr klare Eingabegrenze. Sicherer ist, explizit die für diesen Anwendungsfall erlaubten Werte aus einem Form Request oder DTO zu übernehmen. Berechtigungsrelevante und serverseitig berechnete Felder werden getrennt gesetzt.

Ein Regressionstest sendet zusätzliche privilegierte Felder mit und bestätigt, dass sie ignoriert oder abgelehnt werden. Nur den erfolgreichen Update-Fall zu testen, entdeckt diese Schwachstelle nicht.

// Unsicher: Die Eingabegrenze ist nicht erkennbar.
$user->update($request->all());

// Besser: $request ist ein Form Request mit passenden rules().
$user->update($request->safe()->only(['name', 'timezone', 'locale']));
it('cannot assign an administrative role through the profile endpoint', function () {
    $user = User::factory()->create(['role' => 'member']);

    $this->actingAs($user)->patch('/profile', [
        'name' => 'Updated',
        'role' => 'admin',
        'tenant_id' => 999,
    ])->assertRedirect();

    expect($user->refresh()->role)->toBe('member')
        ->and($user->tenant_id)->not->toBe(999);
});

Empfehlung: Verwenden Sie pro Schreiboperation einen eigenen Form Request oder ein Command-DTO. Allgemeine Methoden wie updateProfile(array $data) sind riskant, wenn sich erlaubte Felder je nach Rolle oder Ablauf unterscheiden.

Dynamische SQL-Bestandteile als normale Parameter behandeln

Eloquent und der Query Builder binden Werte sicher, solange ihre vorgesehenen APIs verwendet werden. Unsicher wird es bei whereRaw(), orderByRaw(), selbst zusammengesetzten Suchausdrücken oder dynamischen Spaltennamen. Bindings schützen Werte, aber keine vom Benutzer kontrollierten SQL-Bezeichner oder SQL-Fragmente.

Sortierung, Filteroperatoren und auswählbare Spalten benötigen feste Allowlisten. Für jeden Client-Wert wird serverseitig ein bekannter Spaltenname oder Operator ausgewählt. Unbekannte Werte werden abgelehnt. Rohes SQL sollte klein, begründet und durch Tests gegen unerwartete Eingaben abgesichert sein.

Die richtige Frage im Review lautet nicht nur „Wird ein Binding verwendet?“, sondern „Kann irgendein Teil der SQL-Struktur vom Request bestimmt werden?“

// Unsicher: direction und column verändern die SQL-Struktur.
$orders = Order::query()
    ->orderByRaw($request->string('column').' '.$request->string('direction'))
    ->get();

// Besser: Client-Begriffe werden auf bekannte Werte abgebildet.
$columns = ['date' => 'created_at', 'total' => 'gross_total'];
$column = $columns[$request->string('sort')->toString()] ?? 'created_at';
$direction = $request->string('direction')->toString() === 'asc' ? 'asc' : 'desc';

$orders = Order::query()->orderBy($column, $direction)->get();

Bei whereRaw() gehören dynamische Werte in Bindings. Dynamische Spalten oder Operatoren brauchen weiterhin eine Allowlist.

Empfehlung: Übersetzen Sie öffentliche Filter- und Sortierbegriffe über feste Mappings in interne Spalten und Operatoren. Raw Expressions bleiben statisch, klein und separat geprüft.

Blade-Ausgabe und gespeicherte Inhalte falsch vertrauen

Blade maskiert {{ $value }} standardmäßig. Mit {!! $value !!} wird diese Schutzgrenze bewusst entfernt. Das ist bei ungeprüften Profiltexten, CMS-Inhalten, Markdown, importierten HTML-Dokumenten oder Fehlermeldungen schnell eine Stored-XSS-Schwachstelle.

Wenn HTML fachlich notwendig ist, braucht es eine serverseitige Sanitization mit einer engen Allowlist. URLs verdienen eine eigene Prüfung: Ein korrekt maskiertes javascript:- oder unerwünschtes data:-Ziel kann weiterhin gefährlich sein. Content Security Policy reduziert Auswirkungen, ersetzt aber keine kontextsensitive Ausgabe.

Tests sollten Payloads in Text, Attributen, Links und strukturierten Inhalten abdecken. Die sichere Behandlung hängt vom Ausgabekontext ab, nicht nur davon, ob Sonderzeichen vorkommen.

{{-- Sicher für gewöhnlichen Text: Blade maskiert die Ausgabe. --}}
<p>{{ $comment->body }}</p>

{{-- Gefährlich bei ungeprüftem Inhalt. --}}
<div>{!! $comment->body !!}</div>

Wenn redaktionelles HTML erlaubt sein muss, sollte ein Sanitizer bereits beim Speichern ein kleines Schema erzwingen. Speichern Sie das bereinigte Ergebnis getrennt vom Original und testen Sie erlaubte Tags, entfernte Event-Handler sowie verbotene URL-Schemata. Markdown muss ebenfalls mit deaktiviertem Raw HTML und sicheren Links gerendert werden.

Empfehlung: Dokumentieren Sie pro Feld, ob es Klartext, Markdown oder bereinigtes HTML enthält. Die Ausgabe erfolgt über eine zentrale Komponente mit Tests für Script-Tags, Event-Attribute, manipulierte Links und SVG.

Uploads wie gewöhnliche Formfelder behandeln

Dateiendung und vom Browser gesendeter Content-Type sind keine belastbare Sicherheitsentscheidung. Ein Angreifer kontrolliert Dateiname, Inhalt und Metadaten. SVG- und HTML-Dateien können aktiven Inhalt enthalten, Bilder können Parser angreifen und öffentlich gespeicherte Originalnamen können Daten verraten oder bestehende Dateien überschreiben.

Uploads benötigen Größenlimit, serverseitige Typprüfung, zufällige Dateinamen und eine bewusste Speicherklasse. Laravels Regeln mimes und mimetypes lassen den erkannten MIME-Typ aus dem Dateiinhalt ableiten; mimes:pdf prüft nicht einfach die vom Benutzer gewählte Endung. Das ist eine Typprüfung, aber kein Beweis, dass ein komplexes Dokument harmlos ist. Vertrauliche Dokumente gehören auf einen privaten Datenträger und werden erst nach Autorisierung über eine kontrollierte Download-Route oder kurzlebige URL ausgeliefert. Bei Bildern kann eine sichere Neucodierung sinnvoll sein.

Laravel erzeugt über store() einen zufälligen Namen und unterstützt private Ablage sowie temporäre URLs. Malware-Prüfung, Inhaltsregeln und Zugriffsprotokollierung müssen passend zum Risiko ergänzt werden.

$data = $request->validate([
    'document' => ['required', 'file', 'mimetypes:application/pdf', 'max:10240'],
]);

$path = $data['document']->store(
    "tenants/{$request->user()->tenant_id}/documents",
    'local',
);

Document::create([
    'tenant_id' => $request->user()->tenant_id,
    'path' => $path,
    // Fachlich bestimmter, serverseitig kontrollierter Anzeigename.
    'download_name' => 'dokument.pdf',
]);

Der Download lädt nicht einfach einen Pfad aus dem Request. Er autorisiert das Datenbankobjekt und verwendet dessen serverseitig gespeicherten Pfad. In einer frischen Laravel-13-Anwendung zeigt der Disk local auf storage/app/private. Ein Disk namens private existiert nur, wenn das Projekt ihn selbst konfiguriert:

public function download(Document $document): StreamedResponse
{
    Gate::authorize('download', $document);

    return Storage::disk('local')->download(
        $document->path,
        $document->download_name,
    );
}

Empfehlung: Aktiver Inhalt wie SVG oder HTML sollte für normale Dokument-Uploads standardmäßig nicht inline von derselben Origin ausgeliefert werden. Nutzen Sie getrennte Hosts oder erzwungene Downloads, wenn eine sichere Transformation nicht möglich ist.

Benutzerdefinierte URLs ohne SSRF-Schutz abrufen

Preview-Generatoren, Webhooks, PDF-Dienste, Importfunktionen und AI-Agenten rufen häufig eine vom Benutzer angegebene URL ab. Ohne Schutz kann der Server damit interne Dienste, Cloud-Metadaten, lokale Admin-Oberflächen oder andere nicht öffentliche Ziele erreichen. Das ist Server-Side Request Forgery.

Eine reine Prüfung auf https:// genügt nicht. Sinnvolle Kontrollen umfassen feste Ziel-Allowlisten, DNS- und IP-Prüfung, blockierte private sowie reservierte Netze, begrenzte Redirects, erneute Prüfung nach jedem Redirect, kurze Timeouts, Größenlimits und kontrollierten Egress. DNS-Rebinding muss im Bedrohungsmodell berücksichtigt werden.

Laravel stellt einen komfortablen HTTP-Client bereit. Die Vertrauensentscheidung für das Ziel bleibt Aufgabe der Anwendung und ihrer Netzwerkinfrastruktur.

// Unsicher: Die Anwendung wird zum frei steuerbaren HTTP-Proxy.
$response = Http::get($request->string('url')->toString());

// Besser für bekannte Integrationen: Ziel serverseitig auswählen.
$clients = [
    'crm' => fn () => Http::baseUrl(config('services.crm.url')),
    'docs' => fn () => Http::baseUrl(config('services.docs.url')),
];

$client = $clients[$request->string('source')->toString()] ?? null;
abort_unless($client, 422);

$response = $client()
    ->connectTimeout(2)
    ->timeout(5)
    ->retry(2, 100)
    ->withoutRedirecting()
    ->get('/approved-path');

withoutRedirecting() verhindert in diesem Beispiel, dass ein erlaubter Host auf ein internes Ziel weiterleitet. Laravels HTTP-Client prüft Redirect-Ziele nicht automatisch als SSRF-Schutz. Müssen beliebige externe URLs oder Redirects unterstützt werden, gehört die Prüfung jedes Zieles in einen eigenen URL-Resolver und zusätzlich in die Netzwerkschicht. Anwendungscode allein kann DNS-Auflösung und wechselnde IPs nur schwer lückenlos kontrollieren.

Empfehlung: Beginnen Sie mit festen Integrationskennungen statt frei eingegebener URLs. Wenn beliebige Ziele fachlich notwendig sind, erzwingt zusätzlich ein Egress-Proxy die Netzwerkgrenzen und blockiert interne sowie reservierte Adressen.

Authentifizierungsabläufe nur auf den Happy Path testen

Login, Passwort-Reset, E-Mail-Wechsel, Zwei-Faktor-Authentifizierung und API-Token sind zusammenhängende Sicherheitsabläufe. Typische Fehler sind Benutzerenumeration durch unterschiedliche Antworten, fehlende Rate Limits, zu lange gültige Links, offene Sessions nach Passwortwechsel oder eine sensible Aktion ohne erneute Authentifizierung.

Sichere Abläufe kombinieren generische Antworten, angemessene Drosselung, kurzlebige und einmalig nutzbare Token, Session-Rotation und eine erneute Bestätigung für besonders kritische Änderungen. Cookie-Einstellungen, Trusted Hosts und Proxy-Konfiguration müssen zur Produktionsumgebung passen.

Tests sollten abgelaufene, bereits verwendete und manipulierte Token sowie parallele Sessions abdecken. Ein erfolgreicher Login-Test sagt wenig über die Widerstandsfähigkeit des gesamten Kontolebenszyklus aus. Laravels Datenbank-Password-Broker löscht den Reset-Token nach einem erfolgreichen Reset. Wenn Ihr Bedrohungsmodell zwei exakt parallele Verwendungen desselben Tokens umfasst, braucht die zustandsverändernde Anwendung zusätzlich eine atomare Einmaligkeitsgrenze.

Route::post('/login', LoginController::class)
    ->middleware('throttle:login');

RateLimiter::for('login', function (Request $request) {
    $key = Str::lower((string) $request->input('email')).'|'.$request->ip();

    return [Limit::perMinute(5)->by(hash('sha256', $key))];
});

Nach erfolgreicher Anmeldung sollte die Session-ID regeneriert werden. Ob eine Passwortänderung andere Sessions und relevante Tokens widerruft, ist eine bewusste Produktentscheidung und muss implementiert werden. Laravel stellt für passwortbasierte Web-Sessions auth.session und Auth::logoutOtherDevices() bereit; persönliche API-Token, OAuth-Token und andere Credential-Stores benötigen eigene Regeln. Für E-Mail-Wechsel, neue MFA-Geräte und Exporte sensibler Daten ist eine aktuelle Passwort- oder MFA-Bestätigung sinnvoll.

Empfehlung: Modellieren Sie Authentifizierung als Zustandsmaschine. Testen Sie jeden Übergang mit abgelaufenen, wiederverwendeten und manipulierten Token sowie parallelen Sessions und wiederholten Recovery-Versuchen.

Webhooks, Jobs und Race Conditions als vertrauenswürdig behandeln

Ein Webhook ist ein öffentlicher Endpunkt, kein interner Funktionsaufruf. Signatur und Zeitfenster müssen vor der Verarbeitung geprüft werden. Replay-Schutz und Idempotenz begrenzen anschließend, was mit einer gültigen, erneut gesendeten Nachricht passieren darf. Das genaue Signaturformat legt der Provider fest. Ein Queue-Job läuft später und möglicherweise auf einem anderen System. Berechtigungen und fachlicher Zustand können sich bis dahin geändert haben.

Bestellungen, Gutscheine, Auszahlungen und Kontingente benötigen außerdem Schutz vor parallelen Requests. Zwei gleichzeitig erfolgreiche Prüfungen können ein Limit doppelt verbrauchen. Datenbanktransaktionen, lockForUpdate(), eindeutige Constraints und verteilte Locks lösen unterschiedliche Teile dieses Problems.

Ein guter Test sendet denselben Webhook mehrfach, verändert die Signatur, verarbeitet Jobs nach einem Rollenwechsel und startet konkurrierende Requests. Sicherheit und Idempotenz müssen unter Wiederholung bestehen, nicht nur beim ersten Durchlauf.

$timestamp = (int) $request->header('X-Webhook-Timestamp');
abort_unless($timestamp > 0 && abs(now()->timestamp - $timestamp) <= 300, 401);

$signedPayload = $timestamp.'.'.$request->getContent();
$signature = hash_hmac('sha256', $signedPayload, config('services.billing.webhook_secret'));
abort_unless(hash_equals($signature, (string) $request->header('X-Signature')), 401);

WebhookReceipt::query()->firstOrCreate(
    ['provider_id' => $request->string('id')->toString()],
    ['payload_hash' => hash('sha256', $request->getContent())],
);

Für zustandsverändernde Verarbeitung reicht firstOrCreate() allein nicht. Eine eindeutige Datenbank-Constraint auf provider_id verhindert konkurrierende Duplikate auf Datenbankebene. Der Handler muss eine mögliche Unique-Constraint-Verletzung des konkurrierenden Requests als bereits empfangen behandeln. Die fachliche Mutation läuft in einer Transaktion:

DB::transaction(function () use ($order): void {
    $lockedOrder = Order::query()->lockForUpdate()->findOrFail($order->id);

    if ($lockedOrder->isPaid()) {
        return;
    }

    $lockedOrder->markAsPaid();
});

Empfehlung: Behandeln Sie Signaturprüfung, Replay-Schutz, Idempotenz und Nebenläufigkeit als vier getrennte Kontrollen. Eine Transaktion allein löst nicht automatisch alle vier Probleme.

Produktionskonfiguration und Secrets als Nebensache behandeln

APP_DEBUG=true, ein zwischen Anwendungen geteilter APP_KEY, Zugangsdaten im Repository, zu breite Cloud-Rechte oder sensible Daten in Logs können eine ansonsten solide Anwendung kompromittieren. Konfigurations-Caches und langlebige Queue-Worker führen außerdem dazu, dass eine Secret-Rotation nicht überall gleichzeitig wirksam wird.

Secrets gehören in einen dafür vorgesehenen Secret Store oder die geschützte Deployment-Umgebung. Rechte werden pro Dienst minimiert, Debugging bleibt in Produktion deaktiviert und Logs werden auf Tokens, Passwörter, personenbezogene Daten und vollständige Request-Inhalte geprüft. Rotation braucht einen getesteten Ablauf für Webserver, Worker und Scheduler.

Ein Deployment-Check sollte sicherheitsrelevante Einstellungen automatisch prüfen. Monitoring muss verdächtige Anmeldeversuche, Autorisierungsfehler, Webhook-Replays und ungewöhnliche Datenexporte sichtbar machen, ohne selbst neue sensible Datensammlungen zu erzeugen.

// Beispiel für einen kleinen, projektbezogenen Deployment-Check.
if (app()->environment('production')) {
    throw_if(config('app.debug'), RuntimeException::class, 'APP_DEBUG must be false.');
    throw_if(blank(config('app.url')), RuntimeException::class, 'APP_URL is required.');
    throw_if(config('session.secure') !== true, RuntimeException::class, 'Secure cookies are required.');
}

Empfehlung: Führen Sie composer audit, Dependency-Updates, Secret-Scanning, Migrationstests und die sicherheitsrelevante Konfigurationsprüfung in CI aus. Das Deployment startet Worker kontrolliert neu, damit neue Konfiguration und rotierte Secrets tatsächlich verwendet werden.

Was ein erfahrenes Review anders macht

Sichere Laravel-Entwicklung besteht nicht aus zehn einzelnen Häkchen. Entscheidend ist, die Vertrauensgrenzen des Produkts zu verstehen: Benutzer, Mandanten, Rollen, Hintergrundprozesse, externe Dienste, Dateien und Deployment. Aus diesen Grenzen entstehen Policies, Validierung, Isolation und negative Tests.

Für ein bestehendes Projekt beginne ich deshalb nicht mit einem pauschalen Scannerlauf. Ich verbinde Architektur- und Codeanalyse mit reproduzierbaren Angriffshypothesen und Tests. Das Ergebnis ist eine priorisierte Antwort auf drei Fragen:

  1. Welche Schwachstelle kann unter realistischen Bedingungen ausgenutzt werden?
  2. Welche gemeinsame Ursache erzeugt möglicherweise weitere Varianten?
  3. Welcher Regressionstest verhindert, dass derselbe Fehler zurückkehrt?

Laravel bietet eine starke Grundlage. Erst sichere fachliche Entscheidungen und Tests machen daraus eine belastbare Plattform.

Sicherheitsarchitektur statt isolierter Einzelkorrekturen

Die zehn Fehler lassen sich zu vier wiederkehrenden Vertrauensgrenzen zusammenfassen:

Grenze Typische Fehler Belastbarer Nachweis
Identität und Sitzung Login, Recovery, E-Mail-Wechsel, MFA, Cookies negative Zustands- und Credential-Tests
Benutzer und Daten IDOR/BOLA, Tenant-Leaks, Mass Assignment Policies sowie Cross-User- und Cross-Tenant-Tests
Anwendung und nicht vertrauenswürdige Eingaben SQL, XSS, Uploads, SSRF kontextsensitive Ausgabe, Allowlisten und isolierter Netzwerkzugang
Anwendung und Betrieb Webhooks, Race Conditions, Secrets, Worker Idempotenz, Datenbank-Invarianten und Deployment-Checks

Ein lokaler Fix ist sinnvoll, wenn er die gemeinsame Ursache schließt. Eine zusätzliche if-Abfrage im betroffenen Controller reicht dagegen selten, wenn derselbe Zugriff über API, Export, Broadcast oder Queue möglich bleibt. Der Regressionstest sollte deshalb die Invariante benennen: „Kein Benutzer darf ein Objekt eines fremden Mandanten erhalten“ ist stärker als „Route X liefert bei ID 43 einen Fehler“.

Für die Vertiefung sind drei eigenständige Leitfäden vorbereitet:

Weiterführende Quellen

Fazit

Die zehn Fehler sind keine voneinander unabhängige Sammlung. Sie entstehen an wiederkehrenden Grenzen zwischen Identität, Daten, Eingaben und Betrieb. Wer diese Grenzen explizit modelliert, über alle Zugriffswege verfolgt und mit negativen Tests belegt, erhält eine Laravel-Anwendung, deren Sicherheit auch bei neuen Funktionen überprüfbar bleibt.

Eine ähnliche Entscheidung steht in Ihrem Projekt an?

Schildern Sie den Kontext. Ich ordne die technischen Optionen, Risiken und einen sinnvollen nächsten Schritt ein.

Projektfrage besprechen ↗