Zum Inhalt springen

CSRF in Symfony und warum Shopware darauf verzichtet

Veröffentlicht am 14. Feb. 2026 | ca. 4 Min. Lesezeit |

Die Security-Komponente gehört zu den mächtigsten und gleichzeitig komplexesten Teilen von Symfony. Einer der am häufigsten missverstandenen Aspekte: CSRF-Schutz. In diesem Artikel gehen wir tief in die Praxis ein — inklusive Shopwares kontroverser Entscheidung, auf CSRF komplett zu verzichten.

Grundkonfiguration der Security-Komponente

Die security.yaml definiert Firewalls, Provider und Access Control. Die Konfiguration folgt dem Zwiebel-Prinzip: Requests durchlaufen mehrere Sicherheitsschichten.

security:
    password_hashers:
        App\Entity\User:
            algorithm: auto

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

    firewalls:
        main:
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: app_login
                check_path: app_login
                enable_csrf: true
            logout:
                path: app_logout

    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/profile, roles: ROLE_USER }

Was CSRF eigentlich ist — und warum es gefährlich wird

Cross-Site Request Forgery nutzt aus, dass der Browser bei jedem Request automatisch alle Cookies mitsendet. Ein Angreifer kann auf einer Fremdseite ein verstecktes Formular platzieren, das den authentifizierten Benutzer unwissentlich eine Aktion auf der Zielseite ausführen lässt — z.B. eine Überweisung, das Ändern der E-Mail-Adresse oder das Löschen von Daten.

Symfonys CSRF-Schutz funktioniert so:

  1. Beim Rendern eines Formulars generiert Symfony ein zufälliges Token
  2. Das Token wird als Hidden-Field in das Formular eingebettet
  3. Beim Submit prüft Symfony, ob das Token gültig ist
  4. Ohne gültiges Token wird der Request abgelehnt
// Im Controller: manueller CSRF-Check
if (!$this->isCsrfTokenValid('delete-item', $request->get('_token'))) {
    throw $this->createAccessDeniedException('Invalid CSRF token.');
}

In Twig-Formularen geschieht das automatisch über {{ form_widget(form._token) }}.

Das Problem: CSRF und moderne JavaScript-SPAs

CSRF-Tokens werden problematisch, wenn die Anwendung stark auf JavaScript-basierte Requests setzt. Typische Probleme:

1. Parallele AJAX-Requests und Session-Locking

Wenn mehrere JavaScript-Fetch-Requests gleichzeitig abfeuern, sendet jeder das gleiche CSRF-Token. Symfony rotiert CSRF-Tokens nicht nach jeder Validierung — das Token bleibt in der Session bestehen. Das eigentliche Problem bei parallelen Requests ist das Session-Locking: PHPs Standard-Session-Handler sperrt die Session-Datei exklusiv, sodass parallele Requests serialisiert werden und aufeinander warten müssen.

// Problem: 3 Requests gleichzeitig, alle mit demselben Token
Promise.all([
    fetch('/api/cart/add', { method: 'POST', body: formData1 }),
    fetch('/api/cart/add', { method: 'POST', body: formData2 }),
    fetch('/api/wishlist/add', { method: 'POST', body: formData3 }),
]);
// Nur der erste Request validiert — die anderen beiden bekommen HTTP 403

2. MySQL Row-Level-Locking bei Sessions

Wenn die Session in der Datenbank gespeichert wird (bei horizontaler Skalierung üblich), führen parallele Requests zu Row-Level-Locks:

Request 1: SELECT ... FROM sessions WHERE id = 'abc' FOR UPDATE  → Lock erhalten
Request 2: SELECT ... FROM sessions WHERE id = 'abc' FOR UPDATE  → Wartet auf Lock
Request 3: SELECT ... FROM sessions WHERE id = 'abc' FOR UPDATE  → Wartet auf Lock

Das serialisiert effektiv alle parallelen Requests eines Benutzers und zerstört die Performance einer SPA.

3. Token-Refresh bei langen Sessions

Bei SPAs, die stundenlang offen bleiben, laufen CSRF-Tokens ab. Man braucht einen Token-Refresh-Endpoint, der selbst wieder CSRF-geschützt sein muss — ein Henne-Ei-Problem.

Shopwares Entscheidung: Kein CSRF

Shopware 6 hat sich bewusst gegen CSRF-Schutz entschieden. Die Gründe sind lehrreich:

1. Store-API-Architektur: Shopware 6 trennt Frontend (Storefront) und Backend (Admin) sauber über APIs. Die Store-API nutzt einen sw-access-key und einen sw-context-token zur Authentifizierung. Diese Token werden nicht automatisch vom Browser gesendet — sie müssen explizit in JavaScript-Code als Header gesetzt werden. Damit ist die Grundvoraussetzung für CSRF-Angriffe nicht gegeben.

2. Performance in Headless-Setups: Shopware wird zunehmend headless eingesetzt (Vue Storefront, Next.js etc.). In diesen Szenarien wäre CSRF-Schutz nicht nur überflüssig, sondern auch ein aktives Hindernis.

3. SameSite-Cookies als Alternative: Moderne Browser unterstützen das SameSite-Cookie-Attribut. Mit SameSite=Lax (Standardwert seit 2020) werden Cookies bei Cross-Origin-Requests nicht mitgesendet — das eliminiert die meisten CSRF-Angriffsvektoren auf Browser-Ebene.

// Shopware setzt stattdessen auf:
// 1. SameSite=Lax Cookies (Default seit 2020)
// 2. sw-context-token Header (kann nicht von Fremdseiten gesetzt werden)
// 3. Content-Type: application/json (unterbindet Simple Requests)

Wann CSRF trotzdem wichtig bleibt

Shopwares Ansatz funktioniert, weil ihre gesamte Architektur darauf ausgelegt ist. Für klassische Symfony-Anwendungen mit serverseitig gerenderten Formularen bleibt CSRF-Schutz unverzichtbar:

  • Formulare ohne JavaScript: Login, Kontaktformulare, Admin-Panels
  • Ältere Browser: SameSite wird nicht überall unterstützt
  • Cookie-basierte Authentifizierung: Wenn der Browser die Auth-Cookies automatisch mitsendet

Custom Authenticator für API-Tokens

Für API-Token-Authentifizierung (ähnlich Shopwares Ansatz) erstellen Sie einen eigenen Authenticator:

<?php

declare(strict_types=1);

namespace App\Security;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;

class ApiTokenAuthenticator extends AbstractAuthenticator
{
    public function supports(Request $request): ?bool
    {
        return $request->headers->has('X-API-TOKEN');
    }

    public function authenticate(Request $request): Passport
    {
        $apiToken = $request->headers->get('X-API-TOKEN');

        return new SelfValidatingPassport(
            new UserBadge($apiToken)
        );
    }

    public function onAuthenticationSuccess(
        Request $request,
        TokenInterface $token,
        string $firewallName
    ): ?Response {
        return null;
    }

    public function onAuthenticationFailure(
        Request $request,
        AuthenticationException $exception
    ): ?Response {
        return new JsonResponse(
            ['error' => 'Authentication failed'],
            Response::HTTP_UNAUTHORIZED
        );
    }
}

Voter für feinkörnige Berechtigungen

Voter ermöglichen Berechtigungsprüfungen auf Objektebene — wesentlich flexibler als einfache Rollen:

<?php

declare(strict_types=1);

namespace App\Security\Voter;

use App\Entity\Article;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class ArticleVoter extends Voter
{
    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, ['EDIT', 'DELETE'])
            && $subject instanceof Article;
    }

    protected function voteOnAttribute(
        string $attribute,
        mixed $subject,
        TokenInterface $token
    ): bool {
        $user = $token->getUser();
        if (!$user instanceof User) {
            return false;
        }

        /** @var Article $article */
        $article = $subject;

        return match ($attribute) {
            'EDIT' => $article->getAuthor() === $user || $user->isAdmin(),
            'DELETE' => $user->isAdmin(),
            default => false,
        };
    }
}

Sicherheits-Checkliste

  • Passwörter — Immer password_hashers: auto verwenden (Argon2id oder bcrypt)
  • Rate Limiting — Login-Versuche begrenzen mit Symfonys RateLimiter-Komponente
  • HTTPS — Erzwingen Sie HTTPS in Produktion (access_control mit requires_channel: https)
  • Content Security Policy — CSP-Header setzen gegen XSS-Angriffe
  • SameSite Cookies — Für APIs auf SameSite=Strict oder Lax setzen
  • CSRF — Bei serverseitigen Formularen aktivieren, bei reinen APIs kritisch evaluieren

Die Symfony Security-Komponente erfordert sorgfältige Konfiguration, bietet dann aber umfassenden Schutz. Der Schlüssel liegt darin, die richtige Strategie für die eigene Architektur zu wählen — Shopwares Beispiel zeigt, dass es keine Einheitslösung gibt.

Thomas Wunner

Thomas Wunner

Fachinformatiker für Anwendungsentwicklung mit Ausbildereignungsprüfung und über 14 Jahre Erfahrung im Aufbau skalierbarer Webanwendungen mit Symfony und Shopware. Abseits der Tastatur ist Thomas als Rettungsschwimmer in der Wasserwacht aktiv, legt als DJ auf und erkundet die Umgebung auf dem Motorrad.

Kommentare

Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.