Zurück zum Blog
März 2026·3 Min. LesezeitKI-generiert
Sprache:DeutschEnglishFrançaisÖsterreichischKlingonisch Originalfassung: English

CORS ein für alle Mal verstehen

#web#http#security

CORS-Fehler tauchen in der Konsole im denkbar ungünstigsten Moment auf, sagen fast nichts Nützliches darüber, wie man sie behebt, und werden „gelöst", indem jemand Access-Control-Allow-Origin: * von Stack Overflow kopiert, ohne dass jemand versteht, was gerade passiert ist. Hier steht, was wirklich vor sich geht und warum der Browser dir das absichtlich antut.

Es ist kein Server-Problem – es ist ein Browser-Problem

Das Erste, was man verinnerlichen muss: CORS (Cross-Origin Resource Sharing) wird ausschließlich vom Browser durchgesetzt, nicht vom Server. Ruft man mit curl einen Endpunkt auf, der im Browser einen „CORS-Fehler" hat, funktioniert er einwandfrei – weil curl CORS gar nicht durchsetzt. Der Server sendet die Antwort immer; der Browser entscheidet, ob dein JavaScript sie lesen darf.

Das gibt es wegen der Same-Origin-Policy: Standardmäßig kann ein Skript auf https://app.example.com keine Antworten von https://api.other.com lesen, obwohl es die Anfrage technisch senden kann. Ohne das könnte jede Website, die du besuchst, stillschweigend in deinem Namen authentifizierte Anfragen an deine Bank stellen, mit Cookies, die dein Browser automatisch mitsendet. CORS ist der Mechanismus, mit dem ein Server bestimmte andere Ursprünge ausdrücklich dafür freigeben kann, seine Antworten zu lesen.

Ursprünge, präzise

Zwei URLs haben nur dann denselben Ursprung, wenn Schema, Host und Port exakt übereinstimmen.

https://app.example.com:443/page1
https://app.example.com:443/page2   → same origin (path doesn't matter)
http://app.example.com               → different origin (scheme differs)
https://api.example.com              → different origin (host differs)
https://app.example.com:8080         → different origin (port differs)

localhost:3000, das in der Entwicklung mit localhost:4000 spricht, ist eine Cross-Origin-Anfrage – weshalb lokale Dev-Setups ständig auf CORS stoßen, obwohl „alles localhost" ist.

Einfache Anfragen vs. Preflight-Anfragen

Nicht jede Cross-Origin-Anfrage löst dasselbe Browserverhalten aus. Eine Anfrage gilt nur dann als „einfach" – und überspringt den zusätzlichen Roundtrip –, wenn sie recht enge Bedingungen erfüllt: Die Methode ist GET, HEAD oder POST; es ist nur eine kleine Allowlist von Headern gesetzt (Accept, Accept-Language, Content-Language, Content-Type); und Content-Type ist application/x-www-form-urlencoded, multipart/form-data oder text/plain.

Alles andere – ein PUT, ein eigener Header wie Authorization oder X-Api-Key, ein JSON-Body mit Content-Type: application/json – löst einen Preflight aus: Der Browser sendet zuerst eine OPTIONS-Anfrage und fragt um Erlaubnis, bevor er die echte sendet.

OPTIONS /api/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, authorization

Der Server muss diesen Preflight ausdrücklich beantworten:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Access-Control-Max-Age ist für die Performance wichtig: Es sagt dem Browser, wie lange in Sekunden er dieses Preflight-Ergebnis cachen darf, bevor er erneut fragt. Ohne das zahlt jedes PUT an eine JSON-API zwei Anfragen statt einer.

Ein minimaler Express-Server, der es richtig macht

app.use((req, res, next) => {
  const allowedOrigins = ['https://app.example.com', 'http://localhost:3000'];
  const origin = req.headers.origin;

  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Vary', 'Origin');
  }

  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.setHeader('Access-Control-Max-Age', '86400');

  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }
  next();
});

Der Header Vary: Origin ist wichtig, wenn diese Antwort irgendwo gecacht werden könnte (ein CDN, ein gemeinsamer Proxy) – er sagt dem Cache, dass der Antwort-Body vom Origin-Header der Anfrage abhängt und dass er eine für einen Ursprung gedachte Antwort nicht an eine Anfrage von einem anderen ausliefern soll.

Credentials sind Opt-in, gleich zweimal

Muss eine Anfrage Cookies oder einen Authorization-Header für eine Sitzung mitsenden, die über Ursprungsgrenzen hinweg lebt, müssen beide Seiten dem ausdrücklich zustimmen:

// Client
fetch('https://api.example.com/orders', {
  credentials: 'include',
});
// Server response
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

Zwei Dinge gehen hier ständig schief: Access-Control-Allow-Origin: * ist laut Spezifikation ausdrücklich nicht erlaubt, wenn Credentials im Spiel sind – man muss einen konkreten Ursprung zurückgeben, keinen Platzhalter –, und wer Access-Control-Allow-Credentials: true auf dem Server vergisst, bei dem verwirft der Browser die Antwort stillschweigend, obwohl die Anfrage selbst erfolgreich war.

Warum * nicht nur eine bequeme Abkürzung ist

Access-Control-Allow-Origin: * sagt dem Browser: „Jede Website im Internet darf diese Antwort lesen." Für eine wirklich öffentliche, nicht authentifizierte API (eine Wetter-API, ein öffentliches CDN-Asset) ist das in Ordnung. Für alles, was nutzerspezifische oder authentifizierte Daten zurückgibt, hebt es den gesamten Zweck der Same-Origin-Policy auf – jede Seite, die der Nutzer besucht, könnte diese Daten nun per Hintergrund-Fetch lesen, wenn irgendwie Credentials angehängt wären. Deshalb blockieren Anfragen mit Credentials den Platzhalter auf Spezifikationsebene ganz, statt darauf zu vertrauen, dass Entwickler die beiden nicht kombinieren.

Fazit

CORS ist kein Fehler deines Servers und kein Hindernis, das der Browser willkürlich aufbaut – es ist der Browser, der durchsetzt, dass Cross-Origin-Lesezugriffe eine ausdrückliche Zustimmung pro Ursprung vom gelesenen Server erfordern. Einfache Anfragen bekommen eine leichtgewichtige Header-Prüfung; alles mit eigenem Header, nichttrivialer Methode oder JSON-Body bekommt zuerst einen Preflight. Wenn man den erlaubten Ursprung, die erlaubten Header und das Credentials-Flag richtig setzt, verschwinden die Fehler, weil der Browser nichts mehr zu beanstanden hat.

NeuerStrukturiertes Logging: Warum printf-Debugging nicht skaliertÄlterRate-Limiting-Algorithmen: Token Bucket vs. Sliding Window