CORS beginnt mit der Same-Origin Policy
Die Same-Origin Policy ist ein grundlegender Sicherheitsmechanismus des Browsers. Sie beschränkt, wie ein Dokument oder Skript Daten eines anderen Origins lesen kann. Unter anderem verhindert sie, dass eine bösartige Website Daten aus einem Dienst ausliest, bei dem der Nutzer bereits angemeldet ist. [3]
CORS, Cross-Origin Resource Sharing, ist ein HTTP-basierter Mechanismus, mit dem ein Server diese Regel gezielt lockern und erlaubte Origins angeben kann. Der Fetch Standard beschreibt CORS als Opt-in-Protokoll. [1][2]
Was ein Origin genau ist
| Seite | Ziel-URL | Beziehung | Warum |
|---|---|---|---|
| https://app.example.com | https://app.example.com/api | Gleicher Origin | Gleiches Schema, Host und Port |
| https://app.example.com | https://api.example.com | Anderer Origin | Anderer Host |
| https://app.example.com | http://app.example.com | Anderer Origin | Anderes Schema |
| https://app.example.com | https://app.example.com:8443 | Anderer Origin | Anderer Port |
| https://app.example.com | https://app.example.com/v2 | Gleicher Origin | Nur der Pfad ändert sich |
Ein Origin wird durch Schema, Host und Port definiert. Zwei URLs sind nur dann same-origin, wenn alle drei Werte übereinstimmen. Der Pfad spielt keine Rolle. [3][4]
`https://app.example.com` und `https://api.example.com` sind daher unterschiedliche Origins. Auch HTTP und HTTPS oder unterschiedliche Ports erzeugen verschiedene Origins. [4]
Same-origin und same-site sind nicht gleichbedeutend. Das ist besonders bei Cookies wichtig, weil `SameSite` auf dem Site-Begriff basiert, CORS dagegen auf Origin. [13]
Was Browser blockiert API tatsächlich bedeutet
Die Aussage, der Browser blockiere den Request, ist oft zu ungenau. Bei einfachen Cross-Origin-Requests kann der Browser den Request senden, eine gültige HTTP-Antwort empfangen und sie anschließend nicht an JavaScript weitergeben, wenn der CORS-Check scheitert. [1][6]
Bei Requests mit Preflight ist der Ablauf anders. Zuerst wird `OPTIONS` gesendet. Schlägt die Prüfung fehl, wird der eigentliche Request nicht gesendet. [2][5][6]
Bei zustandsändernden Operationen ist diese Unterscheidung wichtig. Ein CORS-Fehler bedeutet nicht automatisch, dass das Backend keinen Request erhalten hat.
Einfache Requests: wann kein Preflight nötig ist
| Methode | Beispiel-Header | Einfacher Request | Auswirkung |
|---|---|---|---|
| GET | Accept | Ja | Kein Preflight |
| POST | Content-Type: text/plain | Ja | Kein Preflight |
| POST | Content-Type: application/json | Nein | JSON löst Preflight aus |
| PUT | Content-Type: application/json | Nein | Methode löst Preflight aus |
| GET | Authorization: Bearer ... | Nein | Header löst Preflight aus |
Ein Request gilt nur dann als einfach, wenn die CORS-Safelist-Bedingungen erfüllt sind. Zulässige Methoden sind `GET`, `HEAD` und `POST`. Manuell gesetzte Header müssen safelisted sein, und `Content-Type` darf nur `application/x-www-form-urlencoded`, `multipart/form-data` oder `text/plain` verwenden. [1]
Ein typischer `POST` mit `Content-Type: application/json` ist nicht einfach und löst normalerweise einen Preflight aus. Gleiches gilt für `PUT`, `DELETE` oder benutzerdefinierte Header wie `X-Request-ID`. [1][11]
Preflight OPTIONS: was geprüft wird
Ein Preflight ist ein automatisch erzeugter `OPTIONS`-Request. Der Browser sendet `Origin`, `Access-Control-Request-Method` und bei Bedarf `Access-Control-Request-Headers`. Der Server antwortet mit erlaubten Origins, Methoden und Headern. [2][5]
Soll etwa `DELETE` mit `Authorization` gesendet werden, kann der Browser zuerst prüfen, ob der Origin diese Methode und diesen Header verwenden darf. Erst eine positive Antwort erlaubt den eigentlichen Request. [5]
Ein Preflight verursacht einen zusätzlichen Roundtrip. Sein Ergebnis kann in einem speziellen CORS-Preflight-Cache gespeichert werden, getrennt vom normalen HTTP-Cache. [5][10]
Die wichtigsten CORS-Header
| Header | Richtung | Zweck |
|---|---|---|
| Origin | Request | Origin, der den Request initiiert |
| Access-Control-Allow-Origin | Response | Origin, dessen Code die Antwort erhalten darf |
| Access-Control-Allow-Methods | Preflight-Response | Vom Preflight erlaubte Methoden |
| Access-Control-Allow-Headers | Preflight-Response | Vom Preflight erlaubte Request-Header |
| Access-Control-Allow-Credentials | Response | Freigabe einer Antwort mit Credentials |
| Access-Control-Expose-Headers | Response | Zusätzliche für JavaScript sichtbare Response-Header |
| Access-Control-Max-Age | Preflight-Response | Cache-Dauer des Preflight-Ergebnisses |
| Vary: Origin | Response | Teilt Caches mit, dass die Antwort von Origin abhängt |
`Origin` ist ein Request-Header für den initiierenden Origin. `Access-Control-Allow-Origin` ist ein Response-Header des Servers und entscheidet, ob die Antwort mit Code dieses Origins geteilt werden darf. [1][2][8]
`Access-Control-Allow-Methods` und `Access-Control-Allow-Headers` sind vor allem für Preflight wichtig. `Access-Control-Allow-Credentials` erlaubt das Teilen credentialed responses, und `Access-Control-Expose-Headers` kann zusätzliche Response-Header für JavaScript sichtbar machen. [1][2]
Wenn der Server `Access-Control-Allow-Origin` dynamisch abhängig vom Request setzt, empfiehlt MDN zusätzlich `Vary: Origin`. [1]
`Access-Control-Allow-Origin`: Wildcard oder konkreter Origin
`Access-Control-Allow-Origin: *` erlaubt das Teilen einer Antwort mit jedem Origin, solange keine Credentials verwendet werden. Für eine tatsächlich öffentliche API kann das korrekt sein. [2][8]
Für private oder sensible Endpoints sollte die erlaubte Menge möglichst klein sein. MDN und OWASP empfehlen konkrete Origins statt eines bedingungslosen Wildcards, wenn breite Freigabe nicht erforderlich ist. [14][17]
Bei mehreren erlaubten Origins sollte der Server den eingehenden `Origin` gegen eine Allowlist prüfen und nur den akzeptierten Wert zurückgeben. Blindes Spiegeln beliebiger Origins ist unsicher. [8][16]
Cookies und Credentials: häufige Fehlerquelle
Mit `credentials: "include"` kann Fetch Credentials auch bei Cross-Origin-Requests einbeziehen. Das reicht allein nicht. Der Server muss `Access-Control-Allow-Credentials: true` und einen konkreten `Access-Control-Allow-Origin` zurückgeben. `*` ist dann nicht erlaubt. [2][6][9]
Cookies folgen zusätzlich eigenen Regeln. `SameSite` kann verhindern, dass ein Cookie bei einem Cross-Site-Request gesendet wird, und `SameSite=None` erfordert `Secure`. [13]
CORS und `SameSite` lösen unterschiedliche Probleme. CORS kann korrekt sein, während das Cookie fehlt, oder das Cookie kann gesendet werden, während CORS den JavaScript-Zugriff auf die Antwort verhindert.
Warum `mode: "no-cors"` meistens nichts repariert
`fetch(..., { mode: "no-cors" })` ist keine CORS-Umgehung für Anwendungen, die die API-Antwort lesen müssen. Methoden und Header sind eingeschränkt und die Antwort ist opaque. JavaScript kann weder Body noch Header lesen, der sichtbare Status ist `0`. [6][7][11]
`no-cors` hat Spezialfälle, unter anderem bestimmte Service-Worker-Szenarien, ist aber für einen typischen JSON-API-Call meist die falsche Lösung. [6]
Wenn ein fremdes API kein CORS anbietet, kann ein eigener Backend- oder Proxy-Dienst den Request serverseitig ausführen. [11][12]
Warum curl, Postman oder Backend funktionieren können
Same-Origin Policy und CORS werden vom Browser durchgesetzt. Ein HTTP-Client außerhalb dieses Modells unterliegt nicht demselben CORS-Check. Deshalb kann ein Endpoint in curl oder einem API-Tool funktionieren und aus Browser-JavaScript trotzdem blockiert sein. [2][3][16]
Das zeigt auch, warum CORS keine API-Authentifizierung ist. Ein Nicht-Browser-Client kann einen beliebigen `Origin` senden. OWASP warnt daher davor, den Header als Identitätsnachweis zu verwenden. [16]
Das API braucht weiterhin normale Autorisierung, Tokens, Sessions, Berechtigungsprüfungen und serverseitige Validierung.
CORS ersetzt weder CSRF-Schutz noch Autorisierung
Die Same-Origin Policy beschränkt vor allem Cross-Origin-Lesezugriffe. Formulare und bestimmte einfache Requests können trotzdem cross-origin gesendet werden. Cookie-basierte Anwendungen benötigen deshalb weiterhin CSRF-Schutz. [3][15]
OWASP empfiehlt ausdrücklich, CORS oder `Origin` nicht allein als Zugriffskontrolle für sensible Ressourcen zu verwenden. Authentifizierung und Autorisierung bleiben unabhängig davon notwendig. [16]
CORS beantwortet, ob ein Browser eine Antwort an Code eines Origins weitergeben darf. Es beantwortet nicht, ob ein Nutzer oder Client die Operation ausführen darf.
Häufige Fehler und Diagnose
Ein fehlendes `Access-Control-Allow-Origin` ist ein typischer Fehler. Weitere Ursachen sind nicht erlaubte Methoden, fehlende Einträge in `Access-Control-Allow-Headers`, `*` mit Credentials, fehlerhafte OPTIONS-Antworten oder Redirects im CORS-Ablauf. [11][12]
JavaScript erhält absichtlich nur begrenzte Fehlerdetails. Browser-Konsole und Network-Panel sind für die genaue Diagnose wichtiger. [1][11]
`CORS request did not succeed` kann auch durch DNS, Timeout, Verbindungsfehler, TLS, Mixed Content oder Browser-Erweiterungen verursacht werden. [11]
- Exakten Frontend-Origin prüfen: Schema, Host und Port.
- `OPTIONS` prüfen, falls vorhanden.
- Status und `Access-Control-Allow-*` der Preflight-Antwort kontrollieren.
- Auch die eigentliche Antwort auf CORS-Header prüfen.
- Bei Cookies `credentials`, `Access-Control-Allow-Credentials`, `SameSite` und `Secure` prüfen.
- Redirects, TLS, Mixed Content und Netzwerkfehler kontrollieren.
- Browser-Request mit funktionierendem curl- oder API-Tool-Request vergleichen.
Gefährliche CORS-Konfigurationen
Blindes Spiegeln des eingehenden `Origin` ist gefährlich, wenn credentialed requests sensible Daten liefern. Ein Angreifer kann einen eigenen Origin kontrollieren und versuchen, Antworten im Kontext eines angemeldeten Nutzers zu lesen. [16]
Zu breite reguläre Ausdrücke und Vertrauen in alle Subdomains sind ebenfalls riskant, wenn eine Subdomain übernommen oder von einer anderen Partei kontrolliert werden kann. OWASP empfiehlt exakte Allowlist-Prüfung. [16][17]
`Access-Control-Allow-Origin: *` ist bei vollständig öffentlichen Daten ohne Credentials nicht automatisch eine Schwachstelle. Das Problem ist eine Freigabe, die mehr Daten oder Origins umfasst als beabsichtigt. [8][17]
Performance: Preflight-Kosten und `Access-Control-Max-Age`
Ein Preflight fügt vor dem eigentlichen Request einen zusätzlichen `OPTIONS`-Request hinzu und kann Latenz erhöhen. Das Ergebnis kann mit `Access-Control-Max-Age` im separaten Preflight-Cache gespeichert werden. [5][10]
Ohne Header liegt der Standard laut MDN bei 5 Sekunden. Browser können eigene Obergrenzen setzen. Firefox begrenzt auf 86400 Sekunden, Chromium seit Version 76 auf 7200 Sekunden. [10]
Stabile Policies sollten sinnvoll gecacht werden, aber nicht so lange, dass eine versehentlich zu breite Sicherheitsfreigabe nur schwer schnell zurückgenommen werden kann.
Praktische Konfiguration und Deployment-Checkliste
Zuerst sollte entschieden werden, ob der Endpoint überhaupt cross-origin erreichbar sein muss. Falls nicht, sind keine CORS-Header nötig. Falls ja, sollten konkrete Origins, benötigte Methoden und Header sowie die Notwendigkeit von Credentials definiert werden. [14][17]
Bei dynamischer Allowlist darf nur ein geprüfter Origin zurückgegeben werden, zusammen mit `Vary: Origin`. Bei Credentials sind ein konkreter Origin und `Access-Control-Allow-Credentials: true` erforderlich. Frontend-Code kann diese Serverentscheidung nicht ersetzen. [1][2]
Nach dem Deployment sollten einfacher Request, Preflight, Cookie-Szenario, unerlaubter Origin und echte Fehlerfälle in DevTools getestet werden.
- Ist Cross-Origin-Zugriff wirklich erforderlich?
- Welche exakten Origins sind erlaubt?
- Werden Credentials benötigt?
- Welche Methoden und Request-Header werden gebraucht?
- Erreicht `OPTIONS` die Anwendung und antwortet korrekt?
- Enthält auch die eigentliche Antwort korrekte CORS-Header?
- Ist bei dynamischem Origin `Vary: Origin` gesetzt?
- Verlangt der Endpoint weiterhin normale Authentifizierung und Autorisierung?
- Wurde sowohl ein erlaubter als auch ein unerlaubter Origin getestet?

