{ }
GET
REST API · Security · Symfony · OWASP
REST API Security Review
Auth, Rate Limit, Input-Validierung, SSRF und Mass Assignment

Unsichere REST APIs sind keine Seltenheit – sie entstehen nicht durch schlechte Absichten, sondern durch fehlende Sicherheitsebenen in einer wachsenden Codebasis. Authentifizierungslücken, fehlendes Rate Limiting, ungeprüfte Eingaben, SSRF-Vektoren und Mass-Assignment-Schwachstellen sind die häufigsten Befunde in API-Security-Reviews und lassen sich mit klaren Mustern systematisch schließen.

18 Min. Lesezeit Auth · Rate Limiting · Validierung · SSRF · Mass Assignment Symfony 7 · PHP 8.4 · OWASP API Security Top 10

1. API Security im Kontext: was ein Review leisten muss

Ein REST API Security Review ist keine Checkliste, die man einmalig abarbeitet, sondern ein strukturierter Prozess, der Sicherheitseigenschaften einer API gegen bekannte Angriffsvektoren prüft. Die OWASP API Security Top 10 listet die häufigsten Befunde aus realen API-Sicherheitsvorfällen – und die ersten Plätze werden seit Jahren von denselben Klassen belegt: fehlerhafte Objekt-Level-Autorisierung, kaputte Authentifizierung und übermäßige Datenweitergabe. Diese Angriffsvektoren sind nicht abstrakt, sondern konkrete Implementierungslücken, die in jeder gewachsenen API-Codebasis entstehen können.

Der Unterschied zwischen einer API-Sicherheits-Überprüfung und einem Penetrationstest liegt im Fokus: Ein Review analysiert den Quellcode, die Konfiguration und die Architektur – ein Pentest bestätigt, ob gefundene Schwachstellen tatsächlich ausgenutzt werden können. Beide ergänzen sich. Ein guter API Security Review deckt die strukturellen Schwächen auf, bevor sie in Produktionssystemen ausgenutzt werden. Die folgenden Abschnitte behandeln die fünf häufigsten Befundkategorien mit konkreten Code-Beispielen für Symfony und PHP 8.4.

2. Authentifizierung und Autorisierung richtig implementieren

Authentifizierung und Autorisierung sind die erste Verteidigungslinie jeder REST API – und gleichzeitig die häufigste Quelle kritischer Schwachstellen. Der OWASP-Befund "Broken Object Level Authorization" beschreibt den Fall, in dem ein Endpunkt zwar prüft, ob ein Nutzer eingeloggt ist, nicht aber, ob der Nutzer auf das konkrete Objekt zugreifen darf. Ein Angreifer ändert einfach die ID in der URL und greift auf fremde Ressourcen zu. Die Lösung ist konsequente Autorisierungsprüfung auf Objekt-Ebene, nicht nur auf Route-Ebene.

JWT-Tokens sind in API-Systemen weit verbreitet, werden aber häufig falsch validiert. Häufige Fehler: die Signatur wird nicht geprüft, der alg-Header wird vom Token selbst bestimmt (Algorithm Confusion), oder der Token wird nach dem Logout nicht invalidiert. Für Symfony-APIs empfiehlt sich LexikJWTAuthenticationBundle mit kurzlebigen Access-Tokens (15–60 Minuten) und einem Refresh-Token-Mechanismus. API-Keys müssen gehasht in der Datenbank liegen – niemals im Klartext. Das erlaubt sichere Invalidierung ohne Datenbankexposition.


<?php
// src/Security/ApiKeyAuthenticator.php
// Secure API key authentication with timing-safe comparison

declare(strict_types=1);

namespace App\Security;

use App\Repository\ApiKeyRepository;
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;

final class ApiKeyAuthenticator extends AbstractAuthenticator
{
    public function __construct(
        private readonly ApiKeyRepository $apiKeyRepository,
    ) {}

    public function supports(Request $request): ?bool
    {
        return $request->headers->has('X-Api-Key');
    }

    public function authenticate(Request $request): Passport
    {
        $rawKey = $request->headers->get('X-Api-Key', '');

        // Hash first, then look up — never store raw keys
        $hashedKey = hash('sha256', $rawKey);

        $apiKey = $this->apiKeyRepository->findByHashedKey($hashedKey);

        if ($apiKey === null || !$apiKey->isActive()) {
            throw new AuthenticationException('Invalid or inactive API key.');
        }

        // Check key scopes against required route scopes
        return new SelfValidatingPassport(
            new UserBadge($apiKey->getOwnerIdentifier())
        );
    }

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

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

3. Rate Limiting: Brute Force, Scraping und DDoS abwehren

Rate Limiting schützt eine REST API vor Brute-Force-Angriffen auf Authentifizierungsendpunkte, vor automatisiertem Scraping öffentlicher Daten und vor unbeabsichtigter oder böswilliger Überlastung. Ein API ohne Rate Limiting ist ein offenes System für jeden Angreifer mit einem Skript. Symfony bietet mit dem RateLimiter-Component eine flexible, auf Redis oder Datenbankbackends aufbauende Lösung, die verschiedene Algorithmen unterstützt: Token Bucket für gleichmäßige Lastverteilung, Sliding Window für präzise zeitliche Begrenzungen und Fixed Window für einfache Request-pro-Minute-Grenzen.

Rate Limiting muss differenziert eingesetzt werden: Login-Endpunkte brauchen strenge Limits pro IP und pro Nutzerkonto, öffentliche Lese-APIs großzügigere Limits mit IP-basiertem Sliding Window, schreibende Endpunkte nutzerspezifische Limits. Der Response muss dabei RFC 6585 folgen: HTTP 429 mit Retry-After-Header und aussagekräftigem Body. Rate-Limit-Informationen in Response-Headern (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) helfen API-Konsumenten, ihre Anfragen anzupassen, ohne in Limits zu laufen.


<?php
// src/EventSubscriber/RateLimitSubscriber.php
// Global rate limiting via Symfony RateLimiter

declare(strict_types=1);

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\RateLimiter\RateLimiterFactory;

final class RateLimitSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private readonly RateLimiterFactory $apiLimiter,
        private readonly RateLimiterFactory $loginLimiter,
    ) {}

    public static function getSubscribedEvents(): array
    {
        return [KernelEvents::REQUEST => ['onRequest', 20]];
    }

    public function onRequest(RequestEvent $event): void
    {
        $request = $event->getRequest();

        // Stricter limits for authentication endpoints
        $factory = str_starts_with($request->getPathInfo(), '/api/auth')
            ? $this->loginLimiter
            : $this->apiLimiter;

        $limiter = $factory->create($request->getClientIp());
        $limit   = $limiter->consume(1);

        if (!$limit->isAccepted()) {
            $retryAfter = $limit->getRetryAfter()->getTimestamp() - time();
            $response   = new JsonResponse(
                ['error' => 'Too Many Requests', 'retry_after' => $retryAfter],
                Response::HTTP_TOO_MANY_REQUESTS
            );
            $response->headers->set('Retry-After', (string) $retryAfter);
            $response->headers->set('X-RateLimit-Limit', (string) $limit->getLimit());
            $response->headers->set('X-RateLimit-Remaining', '0');
            $event->setResponse($response);
        }
    }
}

4. Input-Validierung: jede Eingabe ist feindlich

Das Grundprinzip der Input-Validierung in einer REST API ist radikal einfach: Jede Eingabe, die von außen kommt, ist potenziell bösartig – unabhängig davon, ob sie von einem internen Dienst, einem vertrauenswürdigen Partner oder einem öffentlichen Client stammt. Validierung muss in der Tiefe stattfinden: am Eingabepunkt (Request-Parsing), im Domänenobjekt (Invarianten) und an der Datenbankschnittstelle (parametrisierte Queries). Eine einzelne Validierungsschicht ist nie ausreichend.

Symfony Validator mit DTOs als Eingabeobjekte ist das empfohlene Muster. Der Request wird zuerst in ein DTO deserialisiert – das allein filtert bereits falsch strukturierte Eingaben. Dann läuft der Validator gegen das DTO und prüft Typ, Format, Länge, Wertebereich und Geschäftsregeln. SQL-Injection wird durch Doctrine ORM und parametrisierte Queries ausgeschlossen, nicht durch manuelle Filterung. NoSQL-Injection, Command-Injection über Shellbefehle und Path-Traversal sind separate Angriffsklassen, die eigene Gegenmaßnahmen erfordern.


<?php
// src/Dto/CreateProductRequest.php
// Strictly typed DTO with comprehensive validation constraints

declare(strict_types=1);

namespace App\Dto;

use Symfony\Component\Validator\Constraints as Assert;

final class CreateProductRequest
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Length(min: 2, max: 200)]
        #[Assert\Regex(pattern: '/^[\p{L}\p{N}\s\-_.,!]+$/u', message: 'Invalid characters in name.')]
        public readonly string $name,

        #[Assert\NotBlank]
        #[Assert\Positive]
        #[Assert\LessThan(value: 1_000_000)]
        public readonly float $price,

        #[Assert\NotBlank]
        #[Assert\Choice(choices: ['physical', 'digital', 'subscription'])]
        public readonly string $type,

        #[Assert\Valid]
        public readonly ?AddressDto $shippingAddress = null,

        // Explicitly listed allowed fields — no mass assignment possible
        #[Assert\Count(max: 10)]
        #[Assert\All([
            new Assert\Length(max: 50),
            new Assert\Regex(pattern: '/^[a-z0-9_]+$/'),
        ])]
        public readonly array $tags = [],
    ) {}
}

5. SSRF: Server-Side Request Forgery verhindern

Server-Side Request Forgery (SSRF) ist ein Angriffsvektor, der in API-Systemen besonders gefährlich ist, weil er den Server als Proxy für interne Anfragen missbraucht. Ein Angreifer übergibt eine URL als Parameter – etwa für einen Webhook, einen Avatar-Download oder einen Link-Preview-Service – und der Server ruft die URL ab. Zeigt die URL auf http://169.254.169.254/ (AWS Metadata Service), auf interne Dienste im privaten Netzwerk oder auf localhost, kann der Angreifer Zugriff auf Systeme erhalten, die von außen nicht erreichbar sind.

SSRF-Prävention erfordert mehrere Schichten: URL-Whitelist für bekannte externe Dienste, DNS-Auflösung mit anschließender IP-Validierung gegen Private-IP-Ranges (RFC 1918, Link-Local, Loopback), Timeouts und maximale Response-Größen, sowie Redirect-Limitierung oder -Verbot. DNS-Rebinding-Angriffe, bei denen eine externe Domain nach der Validierung auf eine interne IP aufgelöst wird, erfordern, dass die aufgelöste IP beim eigentlichen Request erneut geprüft wird – eine separate DNS-Auflösung reicht nicht aus.


<?php
// src/Service/SafeUrlFetcher.php
// SSRF prevention: DNS resolution + private IP range check

declare(strict_types=1);

namespace App\Service;

use Symfony\Contracts\HttpClient\HttpClientInterface;

final class SafeUrlFetcher
{
    // RFC 1918, Loopback, Link-Local, Multicast
    private const BLOCKED_RANGES = [
        '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16',
        '127.0.0.0/8', '169.254.0.0/16', '0.0.0.0/8',
        '::1/128', 'fc00::/7', 'fe80::/10',
    ];

    public function __construct(
        private readonly HttpClientInterface $httpClient,
    ) {}

    public function fetch(string $url): string
    {
        $parsed = parse_url($url);

        if (!in_array($parsed['scheme'] ?? '', ['http', 'https'], true)) {
            throw new \InvalidArgumentException('Only http/https URLs are allowed.');
        }

        $host = $parsed['host'] ?? '';
        $ip   = gethostbyname($host);

        if ($ip === $host) {
            throw new \RuntimeException('DNS resolution failed.');
        }

        foreach (self::BLOCKED_RANGES as $range) {
            if ($this->ipInCidr($ip, $range)) {
                throw new \RuntimeException("URL resolves to blocked IP range: {$ip}");
            }
        }

        // Re-resolve at request time to prevent DNS rebinding
        $response = $this->httpClient->request('GET', $url, [
            'timeout'          => 5,
            'max_redirects'    => 0, // No redirects — prevents bypass via redirect
            'resolve'          => [$host => $ip], // Pin IP from validation
            'headers'          => ['User-Agent' => 'mironsoft-safe-fetcher/1.0'],
        ]);

        return $response->getContent();
    }

    private function ipInCidr(string $ip, string $cidr): bool
    {
        [$subnet, $prefix] = explode('/', $cidr);
        $ipLong     = ip2long($ip);
        $subnetLong = ip2long($subnet);

        if ($ipLong === false || $subnetLong === false) {
            return false;
        }

        $mask = ~((1 << (32 - (int) $prefix)) - 1);
        return ($ipLong & $mask) === ($subnetLong & $mask);
    }
}

6. Mass Assignment: automatisches Binding kontrollieren

Mass Assignment ist eine Schwachstelle, bei der ein API-Endpunkt eingehende JSON-Felder direkt auf ein Objekt oder eine Datenbankzeile mappt, ohne zu prüfen, welche Felder tatsächlich gesetzt werden dürfen. Ein klassisches Beispiel: ein Nutzer-Profil-Endpunkt akzeptiert {"name": "Alice", "role": "admin"} und setzt das role-Feld, obwohl es nicht im Update-Formular erscheint. Der Angreifer muss nur wissen, wie das interne Modell aussieht – oft durch OpenAPI-Docs oder durch Analyse anderer Endpunkte.

Die Lösung ist konsequente Verwendung von DTOs als Eingabeschicht. Das DTO definiert explizit, welche Felder akzeptiert werden. Es gibt keine automatische Übernahme von Feldern aus dem Request. Der Symfony Serializer mit explizit definierten Gruppen oder readonly Properties im DTO verhindert, dass der Deserializer unbekannte Felder auf Properties mappt. Das Modell-Objekt selbst sollte keine öffentlichen Setter für sicherheitskritische Felder exponieren. Beim Update-Endpunkt verwendet man ein separates UpdateDTO, das nur die erlaubten Felder enthält – nicht dasselbe DTO wie beim Lesen.

Angriffsvektor Risiko Gegenmaßnahme Symfony-Tool
Broken Auth Kritisch Kurzlebige JWTs, Signaturvalidierung, kein alg:none LexikJWTBundle
Brute Force / DDoS Hoch IP- und Account-basiertes Rate Limiting, HTTP 429 RateLimiter Component
Injection (SQL, Shell) Kritisch DTOs + Validator, parametrisierte Queries, kein eval Doctrine ORM + Validator
SSRF Hoch IP-Validierung nach DNS-Auflösung, Redirect-Verbot HttpClient + eigener Validator
Mass Assignment Hoch Explizite DTOs pro Endpunkt, kein direktes Entity-Mapping Serializer + readonly DTOs

8. Zusammenfassung

Ein systematischer REST API Security Review deckt die fünf kritischsten Angriffsvektoren ab: Authentifizierungslücken durch falsch implementierte JWT-Validierung oder Klartextschlüssel, fehlendes Rate Limiting das Brute-Force-Angriffe ermöglicht, ungeprüfte Eingaben die SQL-Injection und Command-Injection öffnen, SSRF durch Server als Proxy für interne Anfragen, und Mass Assignment durch automatisches Binding ohne explizite Feldlisten. Jeder dieser Vektoren hat eine klare Gegenmaßnahme, die mit Symfony-Bordmitteln implementierbar ist.

Der wichtigste Grundsatz: Sicherheit ist eine Eigenschaft des Gesamtsystems, nicht einer einzelnen Komponente. Eine perfekt validierte Eingabe hilft nichts, wenn die Autorisierung auf Objekt-Ebene fehlt. Ein sicheres Rate Limit bringt wenig, wenn JWT-Tokens nicht signiert werden. Security-in-Depth bedeutet, dass jede Schicht – Transport, Authentifizierung, Autorisierung, Validierung, Datenbankzugriff – eigenständige Schutzmaßnahmen implementiert. Regelmäßige Reviews und automatisierte Security-Tests in der CI-Pipeline stellen sicher, dass neue Features keine neuen Schwachstellen einführen.

REST API Security Review — Das Wichtigste auf einen Blick

Authentifizierung

Kurzlebige JWTs, gehashte API-Keys, Objekt-Level-Autorisierung prüfen – nicht nur Route-Level. Algorithm Confusion verhindern.

Rate Limiting

Differenziert nach Endpunkttyp: strenger für Auth-Endpunkte, Sliding Window für öffentliche APIs. HTTP 429 mit Retry-After-Header.

Input-Validierung

DTOs als Eingabeschicht mit Symfony Validator. Mehrere Validierungsebenen. Keine manuelle SQL-Filterung – Doctrine ORM nutzen.

SSRF & Mass Assignment

IP nach DNS-Auflösung validieren, Redirects verbieten. Explizite readonly DTOs pro Endpunkt verhindern Mass Assignment vollständig.

9. FAQ: REST API Security Review

1Unterschied Authentifizierung vs. Autorisierung in APIs?
Authentifizierung klärt die Identität, Autorisierung die Berechtigung. OWASP-Hauptbefund: korrekt authentifizierte Nutzer greifen auf fremde Objekte zu, weil Objekt-Level-Autorisierung fehlt.
2JWT Algorithm Confusion verhindern?
Algorithmus explizit beim Validieren festlegen, nie den alg-Header aus dem Token selbst akzeptieren. alg:none immer ablehnen. LexikJWTBundle mit signature_algorithm konfigurieren.
3Welcher Rate-Limiting-Algorithmus passt für REST APIs?
Token Bucket für normale APIs mit erlaubten Bursts. Sliding Window für Auth-Endpunkte. Fixed Window ist einfacher, aber anfällig für Burst-Angriffe an der Fenstergrenze.
4Reicht clientseitige Validierung?
Nein. Clientseitige Validierung ist reine UX – Angreifer senden Requests direkt an die API. Nur serverseitige Validierung ist sicherheitsrelevant.
5Wo erkenne ich SSRF-anfällige Stellen?
Überall wo der Server eine vom Nutzer kontrollierte URL aufruft: Webhooks, Avatar-Uploads per URL, Link-Previews, OAuth-Callbacks. Suche nach file_get_contents, curl_exec, HttpClient mit Nutzereingabe als URL.
6Schützt DNS-Auflösung vor DNS-Rebinding?
Nein – einmalige Auflösung vor dem Request reicht nicht. Die aufgelöste IP muss beim Request gepinnt werden. Symfony HttpClient bietet die resolve-Option für genau diesen Zweck.
7Was ist Mass Assignment und wie gefährlich ist es?
Request-Body wird ohne Feldfilterung auf ein Objekt gemappt. Angreifer senden role:admin oder is_verified:true. Explizite DTOs mit readonly Properties verhindern es vollständig.
8Objekt-Level-Autorisierung in Symfony?
Symfony Security Voters: Voter bekommt das konkrete Objekt und den Nutzer. Im Controller denyAccessUnlessGranted('EDIT', $product). Nie nur prüfen ob eingeloggt – immer auf das spezifische Objekt prüfen.
9Security-Headers für reine JSON-APIs?
Ja: X-Content-Type-Options, Strict-Transport-Security, X-Frame-Options und Cache-Control: no-store für sensitive Endpunkte. Kosten nichts, helfen gegen bestimmte Angriffsvektoren.
10Automatisierte Security-Tests für APIs?
OWASP ZAP im API-Scan-Modus mit OpenAPI-Spec. PHPStan mit Security-Extensions. Contract-Tests die Autorisierungsregeln prüfen. Fuzzing-Tests für Validierungslogik.