CORS in REST-APIs tiefgehend verstehen und sicher konfigurieren
AI generated
{ }
GET
CORS · Browser-Sicherheit
CORS in REST-APIs tiefgehend verstehen
Warum Access-Control-Allow-Origin: * fast immer die falsche Lösung für ein CORS-Problem ist

CORS-Fehler in der Browser-Konsole gehören zu den frustrierendsten Debugging-Erlebnissen für Frontend-Entwickler, und die naheliegende, schnelle Lösung, Access-Control-Allow-Origin auf * zu setzen, funktioniert zwar meist sofort, untergräbt aber genau den Sicherheitsmechanismus, den CORS eigentlich bereitstellen soll. Ein tieferes Verständnis der zugrunde liegenden Preflight- und Simple-Request-Mechanik erlaubt, CORS gezielt und sicher zu konfigurieren, statt es pauschal abzuschalten.

16 Min. Lesezeit CORS Browser-Sicherheit

1. Die Same-Origin-Policy als eigentliche Ursache des CORS-Problems

CORS existiert nur, weil Browser standardmäßig die Same-Origin-Policy durchsetzen, die verhindert, dass JavaScript-Code von einer Origin (Protokoll, Host und Port zusammen) auf Ressourcen einer anderen Origin zugreift, ohne explizite Erlaubnis. Diese Policy schützt Nutzer davor, dass eine bösartige, im Hintergrund geöffnete Webseite unbemerkt Requests an andere, möglicherweise mit Cookies authentifizierte Webseiten sendet und anschließend deren Antworten ausliest, ein Angriffsmuster, das ohne eine durchgesetzte Same-Origin-Policy technisch trivial umsetzbar wäre und massenhaften Datenabfluss ermöglichen würde.

CORS ist der standardisierte Mechanismus, über den ein Server explizit erlauben kann, dass bestimmte fremde Origins trotzdem auf seine Ressourcen zugreifen dürfen, indem er entsprechende Access-Control-Header in seinen Antworten setzt. Ohne diese explizit gesetzten Header blockiert der Browser den Zugriff des aufrufenden JavaScript-Codes strikt auf die Antwort, selbst wenn der eigentliche HTTP-Request am Server bereits vollständig und erfolgreich verarbeitet wurde.

2. Simple Requests vs. Preflight-Requests

Nicht jeder Cross-Origin-Request löst denselben CORS-Ablauf aus: Ein Simple Request (GET, HEAD, POST mit bestimmten Standard-Content-Types wie application/x-www-form-urlencoded, ohne Custom-Header) wird direkt an den Server gesendet, und der Browser prüft erst bei Erhalt der Antwort, ob die Access-Control-Allow-Origin-Header den Zugriff erlauben. Für alle anderen Requests, insbesondere solche mit application/json-Content-Type oder Custom-Headern wie Authorization, sendet der Browser zunächst automatisch einen OPTIONS-Preflight-Request, um vorab zu klären, ob der eigentliche Request überhaupt erlaubt wäre.

Dieser automatisch ausgelöste Preflight-Request enthält Access-Control-Request-Method und Access-Control-Request-Headers, die dem Server mitteilen, welche konkrete Methode und welche Header der eigentliche, nachfolgende Request nutzen würde, und der Server muss in seiner Preflight-Antwort ausdrücklich bestätigen, dass genau diese Kombination erlaubt ist, bevor der Browser den eigentlichen Request überhaupt tatsächlich absendet.


<?php
declare(strict_types=1);

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

final class CorsSubscriber
{
    private const ALLOWED_ORIGINS = [
        'https://app.example.com',
        'https://admin.example.com',
    ];

    public function onKernelRequest(Request $request): ?Response
    {
        if ($request->getMethod() !== 'OPTIONS') {
            return null;
        }

        $origin = $request->headers->get('Origin');
        if (!in_array($origin, self::ALLOWED_ORIGINS, true)) {
            return new Response('', 403);
        }

        return new Response('', 204, [
            'Access-Control-Allow-Origin' => $origin,
            'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE',
            'Access-Control-Allow-Headers' => 'Content-Type, Authorization',
            'Access-Control-Max-Age' => '3600',
        ]);
    }
}

3. Warum Access-Control-Allow-Origin: * fast immer falsch ist

Der Wildcard-Wert * erlaubt jeder beliebigen Webseite im Internet, per JavaScript auf die API zuzugreifen, was für eine öffentliche, unauthentifizierte API mit rein lesenden, unkritischen Daten akzeptabel sein kann, für jede API mit authentifizierten, nutzerspezifischen Daten aber ein erhebliches Sicherheitsrisiko darstellt. Eine bösartige Webseite könnte in diesem Fall im Hintergrund Requests an die eigentliche API senden und, sofern Session-Cookies automatisch mitgesendet werden, im Namen eines eingeloggten Nutzers agieren.

Der entscheidende technische Schutz dagegen ist, dass der Wildcard-Wert * per Spezifikation explizit NICHT mit Access-Control-Allow-Credentials: true kombinierbar ist, der Browser blockiert diese unsichere Kombination aktiv und konsequent. Sobald eine API also Cookies, Authorization-Header oder andere Credentials für authentifizierte Requests benötigt, MUSS Access-Control-Allow-Origin eine explizite, konkrete Origin statt des Wildcards enthalten, was viele Entwickler in der Praxis leider erst durch einen frustrierend fehlschlagenden Request und anschließende Recherche schmerzhaft lernen.

4. Credentials korrekt über Cross-Origin-Requests handhaben

Für Cross-Origin-Requests, die zusätzlich Cookies oder andere sensible Credentials mitsenden sollen, muss der Client explizit credentials: 'include' in der Fetch-API oder withCredentials: true bei XMLHttpRequest setzen, da Credentials bei Cross-Origin-Requests standardmäßig NICHT automatisch mitgesendet werden. Diese explizite Opt-in-Anforderung ist eine zusätzliche, bewusst eingebaute Sicherheitsebene, die verhindert, dass sensible Credentials versehentlich bei jedem beliebigen Cross-Origin-Request automatisch mitgesendet werden, ohne dass der Entwickler das bewusst und aktiv so konfiguriert hat.

Server-seitig muss bei tatsächlicher Verwendung von Credentials sowohl Access-Control-Allow-Origin (mit einer konkreten Origin, nicht dem Wildcard) als auch Access-Control-Allow-Credentials: true gesetzt werden, andernfalls blockiert der Browser die Antwort trotz technisch erfolgreicher Serververarbeitung, was zu verwirrenden Fehlern führen kann, wenn nur eine der beiden Bedingungen erfüllt ist.

5. Dynamische Origin-Validierung für mehrere erlaubte Domains

Bei mehreren legitim erlaubten Origins gleichzeitig (etwa eine Produktions-App und eine separate, eigenständige Admin-Oberfläche unter unterschiedlichen Subdomains) reicht ein einzelner statischer Access-Control-Allow-Origin-Wert nicht aus, da dieser Header pro Antwort nur eine einzige Origin enthalten kann, nicht eine Liste. Die übliche Lösung ist, den Origin-Header des eingehenden Requests gegen eine serverseitig gepflegte Allowlist zu prüfen und bei einem Treffer genau diese Origin dynamisch in der Antwort zurückzuspiegeln, wie im obigen Code-Beispiel gezeigt.

Diese Allowlist sollte niemals über eine naive, unsichere Präfix- oder Substring-Prüfung erfolgen (etwa "endet mit .example.com"), da eine solche vereinfachte Prüfung leicht durch eine geschickt gewählte, bösartige Domain wie evil-example.com umgangen werden kann, sondern stattdessen immer über einen exakten, vollständigen String-Vergleich gegen eine vollständige, explizit und sorgfältig gepflegte Liste tatsächlich erlaubter Origins.

6. Preflight-Ergebnisse cachen, um Latenz zu reduzieren

Jeder Preflight-Request bedeutet für den Client einen zusätzlichen Roundtrip vor dem eigentlichen Request, was spürbar zur wahrgenommenen Latenz beiträgt, besonders bei APIs mit vielen aufeinanderfolgenden Cross-Origin-Aufrufen. Der Access-Control-Max-Age-Header teilt dem Browser mit, wie lange (in Sekunden) das Ergebnis eines Preflight-Requests für dieselbe Kombination aus Origin, Methode und Headern zwischengespeichert werden darf, ohne einen erneuten Preflight-Request auszulösen.

Ein zu niedrig gewählter Max-Age-Wert erzwingt unnötig häufige, redundante Preflight-Requests, ein zu hoch gewählter Wert verzögert dagegen spürbar die Wirksamkeit späterer CORS-Konfigurationsänderungen, da Browser das einmal gecachte Ergebnis konsequent bis zum tatsächlichen Ablauf weiterhin verwenden. Chrome begrenzt den Wert zusätzlich auf maximal zwei Stunden, unabhängig vom im Header angegebenen Wert.

7. CORS-Fehler systematisch debuggen

Eine CORS-Fehlermeldung in der Browser-Konsole beschreibt fast immer präzise, welche Bedingung nicht erfüllt war (fehlender Header, falscher Origin-Wert, fehlende Credentials-Erlaubnis), weshalb der erste Debugging-Schritt immer sein sollte, die exakte Fehlermeldung sorgfältig zu lesen, statt vorschnell zu Access-Control-Allow-Origin: * zu greifen. Die Netzwerk-Tab-Ansicht der Browser-DevTools zeigt zusätzlich, ob überhaupt ein Preflight-Request gesendet wurde und welche Header dieser tatsächlich enthielt.

Ein häufiger, oft leicht übersehener Fehler ist, dass der Server zwar auf den eigentlichen Request korrekt und vollständig mit CORS-Headern antwortet, aber die separate Preflight-OPTIONS-Anfrage selbst nicht korrekt behandelt (etwa weil eine Authentifizierungs-Middleware den unauthentifizierten OPTIONS-Request fälschlich blockiert, bevor die CORS-Logik überhaupt erreicht wird), was eine separate, oft übersehene Fehlerquelle jenseits der eigentlichen CORS-Header-Konfiguration darstellt.

8. CORS-Header hinter CDN und Reverse-Proxy korrekt durchreichen

Ein CDN oder Reverse-Proxy vor der eigentlichen API kann CORS-Header unbeabsichtigt herausfiltern, überschreiben oder cachen, wenn die Proxy-Konfiguration nicht explizit berücksichtigt, dass diese Header dynamisch pro Origin variieren müssen, statt statisch für alle Requests identisch zu sein. Ein Reverse-Proxy, der Antworten cacht, ohne den Vary-Origin-Header zu respektieren, kann versehentlich die für Origin A berechnete CORS-Antwort auch an Origin B ausliefern, was zu inkonsistentem, schwer nachvollziehbarem CORS-Verhalten führt.

Bei CDN-Einsatz lohnt sich deshalb ein expliziter Test, der Requests von unterschiedlichen, tatsächlich unterschiedlichen Origins gegen dieselbe URL sendet und prüft, ob die CORS-Header korrekt pro Origin variieren, statt sich blind auf eine funktionierende lokale Entwicklungsumgebung ohne CDN davor zu verlassen.

9. CORS-Konfiguration im Überblick

Die folgende Tabelle fasst die wichtigsten Header und ihre Bedeutung zusammen.

Header Zweck Wichtiger Hinweis
Access-Control-Allow-Origin Erlaubte Origin(s) für den Zugriff Niemals * bei Credentials kombinieren
Access-Control-Allow-Credentials Erlaubt Cookies/Auth bei Cross-Origin Erfordert konkrete Origin, kein Wildcard
Access-Control-Allow-Headers Erlaubte Custom-Header im echten Request Muss Preflight-Anfrage-Header abdecken
Access-Control-Max-Age Cache-Dauer für Preflight-Ergebnis Chrome begrenzt auf max. 2 Stunden

Mironsoft

OpenAPI-Design, Symfony-APIs und API-Sicherheit

APIs, die externe Teams ohne Rückfragen integrieren können?

Wir prüfen bestehende REST-APIs auf inkonsistente Fehlerformate, fehlende OpenAPI-Dokumentation und Sicherheitslücken und bauen daraus eine API, die klar dokumentiert, versioniert und gegen Missbrauch abgesichert ist.

API-Review

OpenAPI-Spezifikation, Fehlerformate und Statuscodes auf Konsistenz prüfen.

Symfony-Umsetzung

DTOs, Serializer und Validator für saubere, typsichere Request/Response-Modelle einsetzen.

Security-Audit

Rate-Limiting, Auth-Schemes und Input-Validierung gegen echte Angriffsflächen absichern.

10. Zusammenfassung

CORS: Das Wichtigste auf einen Blick

Same-Origin-Policy

CORS existiert als kontrollierter Ausnahmemechanismus zur browserseitigen Same-Origin-Policy.

Preflight-Mechanik

Nicht-einfache Requests lösen einen automatischen OPTIONS-Preflight vor dem eigentlichen Request aus.

Wildcard-Gefahr

Access-Control-Allow-Origin: * ist unvereinbar mit Credentials und für authentifizierte APIs ungeeignet.

Dynamische Allowlist

Mehrere erlaubte Origins erfordern serverseitige Prüfung und dynamisches Zurückspiegeln der Origin.

11. FAQ: CORS: Das Wichtigste auf einen Blick

1Ist CORS eine Server- oder eine Client-seitige Sicherheitsmaßnahme?
Beides: Der Server setzt die Header, aber durchgesetzt wird die Policy ausschließlich vom Browser des Clients, nicht vom Server selbst.
2Warum sehe ich den CORS-Fehler nicht in Tools wie Postman?
Weil Postman kein Browser ist und die Same-Origin-Policy nicht durchsetzt. CORS ist ein reines Browser-Sicherheitskonzept.
3Kann ich CORS für interne, nicht-öffentliche APIs einfach deaktivieren?
CORS lässt sich nicht global deaktivieren, aber für rein server-zu-server genutzte APIs ohne Browser-Zugriff ist es ohnehin irrelevant.
4Was passiert, wenn ich Access-Control-Allow-Headers vergesse?
Der Preflight-Request schlägt fehl, wenn der eigentliche Request Header enthält, die nicht in dieser Liste stehen, etwa Authorization.
5Löst CORS auch Probleme bei Server-zu-Server-Kommunikation?
Nein, CORS betrifft ausschließlich Browser-initiierte Requests. Server-zu-Server-Aufrufe sind von der Same-Origin-Policy nicht betroffen.
6Wie handhabe ich CORS für eine API mit vielen dynamischen Subdomains?
Über eine Musterprüfung mit korrektem Regex-Anker (nicht naives endsWith), die Subdomains einer registrierten Basisdomain zuverlässig erkennt.
7Sind CORS-Fehler ein Zeichen für einen Bug im Backend?
Meist eine fehlende oder fehlerhafte CORS-Konfiguration, kein funktionaler Bug in der eigentlichen Geschäftslogik der API.
8Muss jeder Endpoint eigene CORS-Header setzen?
Nein, eine zentrale Middleware oder ein Subscriber, der für alle Endpoints gilt, ist der übliche und wartungsärmere Ansatz.
9Wie teste ich CORS-Konfiguration automatisiert?
Mit Integrationstests, die Requests mit unterschiedlichen Origin-Headern senden und die resultierenden Access-Control-Header prüfen.
10Beeinflusst CORS die Performance der API selbst?
Nur indirekt über zusätzliche Preflight-Roundtrips beim Client, die eigentliche Server-Verarbeitungszeit bleibt davon unberührt.