Cross-Origin Resource Sharing: Der Türsteher des modernen Webs

Same-Origin-Policy, Preflight und Credentials verständlich erklärt – und wie Sie CORS-Fehler sauber auf dem Server beheben
Aktualisiert:
Abstract
- #CORS
- #Cross-Origin Resource Sharing
- #Webentwicklung
- #Sicherheit
- #Same-Origin-Policy
- #Origin-Header
- #Access-Control-Allow-Origin
- #Preflight-Requests
- #Sicherheitsmechanismus
- #Fetch API
Warum der Browser Ihre API-Antwort blockiert – und was wirklich dagegen hilft
Die neue Shop-Oberfläche läuft lokal auf dem Rechner der Entwicklerin, die API steht auf dem Testserver, und der erste Aufruf endet in roter Schrift in der Browserkonsole: blocked by CORS policy. Im Netzwerk-Tab sieht die Antwort des Servers dabei völlig normal aus, Statuscode 200, Daten vorhanden. Trotzdem kommt im JavaScript-Code nichts an.
Kaum eine Fehlermeldung kostet Webteams so viel Zeit wie diese, und kaum eine wird so oft falsch „repariert“. Das liegt daran, dass CORS kein Fehler im eigentlichen Sinn ist, sondern eine Schutzregel des Browsers. Wer verstanden hat, wen sie schützt und wer sie lockern darf, löst die meisten Probleme in wenigen Minuten und vermeidet dabei Konfigurationen, die neue Sicherheitslücken öffnen.
Die Same-Origin-Policy: Warum Browser fremde Antworten zurückhalten
Ausgangspunkt ist die Same-Origin-Policy, eine der ältesten Sicherheitsregeln im Browser. Sie besagt: Skripte einer Webseite dürfen grundsätzlich nur Antworten von Servern derselben Origin lesen.
Eine Origin besteht aus drei Teilen: Schema (https), Host (shop.example.de) und Port (443). Nur wenn alle drei übereinstimmen, gilt eine Adresse als gleiche Origin. Schon https://example.de und https://api.example.de sind zwei verschiedene Origins, ebenso http://localhost:3000 und http://localhost:8080.
Warum diese Strenge? Angenommen, Sie sind in einem Tab bei Ihrem Online-Banking angemeldet und öffnen in einem anderen Tab eine präparierte Seite. Ohne Same-Origin-Policy könnte deren JavaScript im Hintergrund Ihr Konto abfragen. Der Browser würde die Anfrage samt Ihrer Anmelde-Cookies senden, und das Skript könnte Kontostand und Umsätze auslesen. Die Same-Origin-Policy verhindert genau das: Die Anfrage darf zwar unter Umständen hinausgehen, aber die fremde Seite bekommt die Antwort nicht zu sehen.
Nicht jede Einbindung fremder Inhalte ist betroffen. Bilder, Stylesheets, Skripte oder Videos von anderen Servern lädt der Browser seit jeher ohne CORS. Erst wenn ein Skript die Inhalte auslesen will, etwa Pixel eines fremden Bilds aus einem Canvas-Element oder die Antwort eines fetch()-Aufrufs, greift die Regel. Ausnahmen sind Webfonts und JavaScript-Module (<script type="module">): Sie brauchen von fremden Servern passende CORS-Header.
Was CORS leistet: kontrollierte Ausnahmen
Moderne Webanwendungen bestehen oft aus mehreren Teilen auf verschiedenen Adressen: Frontend auf www, API auf api, Bilder auf einem CDN, Anmeldung bei einem Identitätsanbieter. Hier kommt Cross-Origin Resource Sharing ins Spiel. CORS ist ein Protokoll, mit dem ein Server dem Browser mitteilt, welchen fremden Origins er das Lesen seiner Antworten erlaubt.
Das ist der wichtigste Punkt zum Verständnis: Nicht die Webseite fordert sich Zugriff an, sondern der Server, dessen Daten gelesen werden, stellt die Erlaubnis aus. Der Browser setzt diese Erlaubnis nur durch. Das Bild vom Türsteher passt daher gut: Der Browser steht an der Tür, die Gästeliste schreibt der Server.
Spezifiziert ist CORS heute im Fetch Standard der WHATWG, der beschreibt, wie Browser Anfragen stellen und verarbeiten.
So läuft eine CORS-Anfrage ab
Einfache Anfragen
Bei vielen alltäglichen Anfragen schickt der Browser die Anfrage direkt ab und ergänzt einen Origin-Header, der verrät, von welcher Seite sie stammt:
GET /api/produkte HTTP/1.1
Host: api.example.de
Origin: https://shop.example.de
Der Server antwortet und gibt in der Antwort an, ob diese Origin lesen darf:
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://shop.example.de
Vary: Origin
Stimmt der Wert in Access-Control-Allow-Origin mit der Origin der Seite überein oder steht dort ein Sternchen (*), reicht der Browser die Antwort an das Skript weiter. Fehlt der Header oder passt er nicht, verwirft der Browser die Antwort und meldet einen CORS-Fehler. Der Header Vary: Origin sagt Caches und CDNs, dass die Antwort je nach anfragender Origin unterschiedlich ausfallen kann. Fehlt er, kann ein Cache die Freigabe für die eine Origin an eine andere ausliefern.
Als „einfach“ gelten Anfragen mit den Methoden GET, HEAD oder POST, die nur unkritische Header verwenden. Als Content-Type sind zudem nur text/plain, multipart/form-data und application/x-www-form-urlencoded erlaubt.
Preflight-Anfragen
Alles andere gilt als potenziell folgenreich: Methoden wie PUT, PATCH oder DELETE, eigene Header wie Authorization oder ein Content-Type von application/json. Damit ist praktisch jeder Aufruf einer modernen JSON-API betroffen. Bevor der Browser solche Anfragen sendet, fragt er beim Server mit einer OPTIONS-Anfrage vor, dem sogenannten Preflight:
OPTIONS /api/bestellungen/42 HTTP/1.1
Origin: https://shop.example.de
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
Der Server muss ausdrücklich zustimmen:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://shop.example.de
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600
Erst danach geht die eigentliche PUT-Anfrage hinaus. Mit Access-Control-Max-Age legt der Server fest, wie viele Sekunden der Browser das Ergebnis des Preflights zwischenspeichern darf. Browser begrenzen diesen Wert allerdings selbst, Chromium etwa auf zwei Stunden. Ohne Cache verdoppelt der Preflight die Zahl der Anfragen, was sich bei mobilen Verbindungen spürbar auf die Ladezeit auswirkt.
Anfragen mit Anmeldedaten
Sollen Cookies oder HTTP-Authentifizierung mitgeschickt werden, muss das Frontend das ausdrücklich anfordern, bei fetch() mit der Option credentials: 'include'. Der Server muss dann zwei Bedingungen erfüllen: Er sendet Access-Control-Allow-Credentials: true, und in Access-Control-Allow-Origin steht die konkrete Origin, kein Sternchen. Die Kombination aus Wildcard und Anmeldedaten lehnt der Browser ab. Das ist eine bewusste Sperre gegen die gefährlichste Fehlkonfiguration.
Ein verwandtes Detail: Auch Antwort-Header sind für Skripte zunächst nur eingeschränkt sichtbar. Möchte das Frontend etwa einen eigenen Header wie X-Total-Count für die Seitennavigation auswerten, muss der Server ihn über Access-Control-Expose-Headers freigeben.
Typische CORS-Fehler und ihre Ursachen
Die Meldungen in der Konsole sind knapp, verweisen aber fast immer auf eine dieser Ursachen:
- Header fehlt ganz: Der Server ist nicht für CORS konfiguriert. Häufig ist das auch dann der Fall, wenn die Anwendung mit einem Fehler (Status 500) antwortet: Viele Frameworks setzen CORS-Header nur bei erfolgreichen Antworten. Der eigentliche Serverfehler versteckt sich dann hinter der CORS-Meldung.
- Origin passt nicht: Ein Tippfehler, ein abschließender Schrägstrich,
httpstatthttps, ein fehlender Port oderwwwgegen ohnewww. Der Vergleich ist exakt. - Preflight scheitert: Der Server kennt keine OPTIONS-Route, antwortet mit 404 oder 405 oder erlaubt die verwendete Methode beziehungsweise den Header nicht.
- Authentifizierung blockiert den Preflight: Preflight-Anfragen enthalten keine Anmeldedaten. Prüft eine Middleware jede Anfrage auf ein gültiges Token, beantwortet sie den Preflight mit 401, und die eigentliche Anfrage findet nie statt. OPTIONS-Anfragen müssen daher vor der Authentifizierung behandelt werden.
- Wildcard mit Credentials:
Access-Control-Allow-Origin: *zusammen mit Cookies funktioniert nicht, siehe oben. - Header doppelt gesetzt: Wenn sowohl der Webserver (etwa Nginx) als auch die Anwendung CORS-Header ergänzen, enthält die Antwort zwei Werte. Der Browser akzeptiert das nicht.
So gehen Sie bei der Fehlersuche vor: Öffnen Sie die Entwicklerwerkzeuge des Browsers, filtern Sie im Netzwerk-Tab nach der betroffenen Anfrage und schauen Sie, ob ihr eine OPTIONS-Anfrage vorausgeht. Vergleichen Sie dann den Origin-Header der Anfrage mit den Access-Control-*-Headern der Antwort. Meist ist der Unterschied damit sichtbar.
CORS richtig konfigurieren: ein Beispiel mit Express
Die Lösung liegt fast immer auf dem Server, der die Daten bereitstellt. Für Node.js mit Express gibt es dafür das verbreitete Paket cors. Statt es pauschal für alle Origins zu öffnen, sollten Sie eine Liste erlaubter Origins hinterlegen:
import express from 'express';
import cors from 'cors';
const app = express();
// Nur diese Frontends dürfen Antworten der API lesen.
const erlaubteOrigins = [
'https://shop.example.de',
'https://admin.example.de',
];
app.use(
cors({
origin: erlaubteOrigins,
credentials: true, // Cookies erlauben
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 600, // Preflight 10 Minuten zwischenspeichern
}),
);
app.get('/api/produkte', (req, res) => {
res.json([{ id: 1, name: 'Akkuschrauber' }]);
});
app.listen(8080);
Die Middleware beantwortet Preflight-Anfragen automatisch und setzt Vary: Origin. Wichtig ist die Reihenfolge: Sie muss vor der Authentifizierung und vor den Routen eingebunden sein. Weitere Schutzmaßnahmen für Express-Anwendungen beschreibt der Beitrag zur Absicherung von Node.js- und Express-Anwendungen.
Andere Frameworks haben vergleichbare Bausteine. In ASP.NET Core etwa definieren Sie eine benannte CORS-Richtlinie und binden sie über die Middleware ein, Django nutzt dafür üblicherweise das Paket django-cors-headers, und Spring bietet eine eigene CORS-Konfiguration. Wer mit .NET arbeitet, findet auf unserer Seite zu ASP.NET Core Unterstützung bei Architektur und Konfiguration.
Wenn Sie den Server nicht kontrollieren
Liegt die API bei einem Drittanbieter, der keine CORS-Freigabe vorsieht, führt kein sauberer Weg direkt aus dem Browser dorthin. Die übliche Lösung ist ein eigener Server, der die Anfrage stellvertretend ausführt (Backend for Frontend oder Proxy). Server-zu-Server-Aufrufe unterliegen keiner Same-Origin-Policy, weil kein Browser beteiligt ist. Das hat einen zweiten Vorteil: API-Schlüssel bleiben auf dem Server und landen nicht im JavaScript-Code, den jeder Besucher lesen kann.
Für die Entwicklung bieten Werkzeuge wie Vite oder Next.js Proxy- oder Rewrite-Funktionen, mit denen das lokale Frontend API-Aufrufe über die eigene Origin leitet. Auch in Produktion lässt sich das Problem oft ganz vermeiden, indem ein Reverse Proxy wie Nginx Frontend und API unter derselben Domain ausliefert, etwa die API unter /api.
Was CORS nicht leistet
An dieser Stelle entstehen die meisten Missverständnisse, und sie sind sicherheitsrelevant.
CORS schützt nicht den Server. Es schützt Nutzerinnen und Nutzer davor, dass fremde Webseiten in ihrem Namen Daten auslesen. Ein Angreifer mit curl oder einem eigenen Skript ignoriert CORS vollständig. Eine API braucht deshalb immer eine eigene Authentifizierung und Autorisierung, unabhängig von der CORS-Konfiguration.
CORS verhindert nicht, dass Anfragen ankommen. Einfache Anfragen schickt der Browser ab, bevor er die Antwort prüft. Ein Formular-POST einer fremden Seite erreicht den Server also durchaus und kann dort etwas auslösen, auch wenn die Seite die Antwort nicht lesen darf. Gegen solche Cross-Site Request Forgery (CSRF) helfen andere Mittel: Cookies mit dem Attribut SameSite, CSRF-Tokens oder die Prüfung des Origin-Headers auf dem Server.
mode: 'no-cors' ist keine Lösung. Die Option in fetch() unterdrückt zwar die Fehlermeldung, liefert aber eine „undurchsichtige“ Antwort, deren Inhalt das Skript nicht lesen kann. Sie ist nur für Spezialfälle gedacht.
Browser-Erweiterungen und abgeschaltete Sicherheit helfen höchstens beim schnellen Test auf dem eigenen Rechner. Für Anwenderinnen und Anwender ändert sich dadurch nichts, und das Problem taucht in Produktion wieder auf.
Schließlich gilt: Eine zu großzügige CORS-Konfiguration kann selbst zur Lücke werden. Besonders riskant ist es, die anfragende Origin ungeprüft in Access-Control-Allow-Origin zurückzuspiegeln und gleichzeitig Credentials zu erlauben. Dann darf faktisch jede Webseite im Namen angemeldeter Nutzer Daten lesen. Ähnlich heikel sind Prüfungen per Teilstring, bei denen etwa example.de.angreifer.net als erlaubt durchrutscht. Zusammen mit einer Cross-Site-Scripting-Lücke auf einer der erlaubten Origins wird CORS außerdem wirkungslos. Wie Sie XSS vermeiden, zeigt der Beitrag zur sicheren JavaScript-Entwicklung gegen Cross-Site-Scripting.
Was das für Ihr Team bedeutet
CORS ist weder Schikane noch Allheilmittel. Es ist ein schmaler, präziser Mechanismus, der eine bestimmte Klasse von Angriffen im Browser verhindert, und er funktioniert gut, wenn man ihn als Teil der API-Konfiguration versteht statt als lästiges Hindernis.
Für die meisten Teams lohnen sich drei Gewohnheiten. Erstens gehört die CORS-Konfiguration zur API und wird dort mit einer expliziten Liste erlaubter Origins gepflegt, getrennt nach Entwicklung, Test und Produktion. Zweitens sollte jede Lockerung, insbesondere * oder erlaubte Credentials, eine bewusste und dokumentierte Entscheidung sein. Drittens lohnt sich die Frage, ob Cross-Origin-Zugriffe überhaupt nötig sind: Eine Architektur, die Frontend und API unter einer Domain bündelt, umgeht viele Probleme von vornherein.
Unsere Einschätzung: Überschätzt wird CORS häufig als Schutz der eigenen API, unterschätzt wird das Risiko einer zu offenen Konfiguration. Wer Sicherheitsfragen wie diese schon in der Architekturphase klärt, spart später aufwendige Korrekturen. Wie sich das in Entwicklungsprozesse einbauen lässt, beschreibt unsere Seite zu Security by Design.
Häufige Fragen zu CORS
Lassen sich CORS-Fehler im Frontend beheben?
Nein. Das Frontend kann lediglich festlegen, ob es Anmeldedaten mitsenden möchte. Ob die Antwort gelesen werden darf, entscheidet allein der Server über seine Header. Ohne Zugriff auf den Server bleibt nur der Umweg über einen eigenen Proxy.
Ist Access-Control-Allow-Origin: * gefährlich?
Für wirklich öffentliche Daten ohne Anmeldung, etwa einen Wetterdienst oder öffentliche Produktinformationen, ist die Wildcard vertretbar. Für alles, was personenbezogene oder interne Daten liefert, sollten Sie eine explizite Liste erlaubter Origins verwenden. In Kombination mit Credentials lässt der Browser die Wildcard ohnehin nicht zu.
Warum funktioniert die Anfrage in Postman, aber nicht im Browser?
Postman, curl und andere Werkzeuge sind keine Browser und setzen die Same-Origin-Policy nicht durch. Die Anfrage selbst ist dort also in Ordnung, nur die Freigabe für die Webseite fehlt. Prüfen Sie deshalb die Antwort-Header und den Preflight.
Betrifft CORS auch Single-Page-Applications?
Ja, gerade dort. SPAs mit React, Vue oder Angular rufen ihre Daten meist per fetch() von einer API ab, die oft auf einer anderen Subdomain liegt. Ohne korrekte CORS-Konfiguration oder einen gemeinsamen Reverse Proxy funktioniert dann kein einziger Datenabruf.
Kategorien
Schlagworte