Zwei Uhr morgens. Sie haben Create React App lokal gestartet, das Frontend läuft auf Port 3000, die Backend-API auf Port 8000. Sie schreiben Ihren ersten fetch()-Aufruf, und die Chrome-Konsole zeigt eine rote Zeile: »has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.« Sie öffnen Google, tippen »CORS error fix«, die erste Stack-Overflow-Antwort sagt »fügen Sie Access-Control-Allow-Origin: * auf dem Server hinzu«. Sie tun es, die rote Zeile verschwindet, Weltfrieden. Was diese Konfiguration genau tut, warum ein serverseitiger Header das Verhalten auf der Browserseite steuert — Sie sind sich nicht ganz sicher und haben auch kein großes Interesse, es herauszufinden. Schließlich läuft der Code ja. Dieses Szenario spielt sich jede Sekunde auf diesem Planeten ab. Zwanzig Jahre später ist CORS immer noch derjenige Sicherheitsmechanismus der Webentwicklung, der am leichtesten »repariert, aber nicht verstanden« wird. Im Juni 2026 tauchte ein alter Artikel von 2019 mit dem Titel »Developers don’t understand CORS« auf Hacker News wieder auf, erreichte 353 Punkte und 251 Kommentare. Die beiden am höchsten bewerteten Kommentare stehen in direktem Gegensatz zueinander, und der Thread darunter erstreckt sich über zweihundert Ebenen, wobei beide Seiten ausführlich Quellen zitieren, ohne einander jemals zu überzeugen. Noch bezeichnender: Das Ziel der Kritik ist genau dieser Erklärartikel selbst — »Even TFA (The F*ing Article) seemingly doesn’t understand CORS.« Der Autor, der CORS erklärt, irrt sich selbst in seinen Erklärungen.
Was CORS eigentlich tut
Um diesen Streit zu verstehen, muss man zur grundlegendsten Frage zurückkehren: Wofür ist CORS eigentlich da? Das Bild, das ich aus der technischen Literatur und den Spezifikationen rekonstruiert habe, sieht etwa so aus: CORS (Cross-Origin Resource Sharing) ist ein vom Browser implementiertes Protokoll, um unter bestimmten Bedingungen die Beschränkungen der Same-Origin Policy (SOP) zu lockern. Lockern, nicht verschärfen. Die SOP ist der eingebaute Sicherheitssockel des Browsers: Standardmäßig kann JavaScript, das von example.com geladen wurde, keine Anfrage an bank.com senden und die Antwort lesen. Diese Standardrichtlinie schützt den Nutzer — wenn Sie bei Ihrer Online-Bank eingeloggt sind und der Browser Ihre Authentifizierungs-Cookies speichert, kann keine andere von Ihnen besuchte Website heimlich Ihre Bankdaten lesen. Die Aufgabe von CORS ist es, dem Server einen Mechanismus zu geben, um zu sagen: »bestimmte andere Origins dürfen meine Antworten lesen« — über Response-Header wie Access-Control-Allow-Origin.
Schon der Name verrät seine Funktion: Es geht um Teilen (Sharing), nicht um Blockieren. Aber genau dieser Name ist die Quelle massiver Verwirrung — wenn ein Entwickler die Konsolenfehlermeldung »blocked by CORS« sieht, ist seine intuitive Reaktion: »CORS hindert mich an etwas.« In Wirklichkeit ist es die SOP, die blockiert; CORS ist der Mechanismus, mit dem der Browser prüft, ob der Server beim Cross-Origin-Versuch die Freigabe erteilt hat. Wenn der Server keine Freigabe erteilt hat, verhält sich der Browser mit »Antwort-lesen verboten« (und bei nicht-einfachen Anfragen: gar nicht erst senden). Aber dieses »Nein« wird dem Namen »CORS« zugeschrieben. Die Diskrepanz zwischen Name und Verhalten ist die erste Einzahlung auf das Kapital der kognitiven Schuld.
Zwei Versionen von »richtig«, zweihundert Ebenen Kampf
Die zentrale Divergenz der HN-Diskussion lässt sich auf die Konfrontation zweier Kommentare verdichten. Der erste, von Nutzer muvlon (der am höchsten bewertete, vor 17 Stunden veröffentlicht), sagt sinngemäß: Dieser Artikel selbst hat CORS nicht verstanden — CORS verhindert keine Anfragen, es lockert nur die Standardbeschränkungen. JavaScript von jeder beliebigen Website kann Anfragen an Ihr localhost:19421 senden. Der Access-Control-Allow-Origin-Header bestimmt nur, ob die Antwort gelesen werden kann oder nicht; die Anfrage selbst wird in jedem Fall gesendet. Der zweite, von Nutzer stymaar (vor 12 Stunden), widerspricht direkt: Nein, was Sie sagen, ist falsch — für sichere Methoden wie GET wird die Anfrage tatsächlich gesendet, aber GET soll idempotent sein, und dass die Antwort nicht gelesen werden kann, ist der gesamte Schutz. Für nicht-idempotente Anfragen sendet der Browser zuerst einen OPTIONS-Preflight; wenn die Preflight-Antwort nicht die richtigen CORS-Header enthält, sendet der Browser die eigentliche Anfrage überhaupt nicht.
Keiner von beiden redet Unsinn. Jeder hat in seinem eigenen Szenario recht. Muvlons Szenario deckt die »einfachen Anfragen« (simple requests) ab — diejenigen, die keinen Preflight auslösen: GET, HEAD, POST (mit Content-Type application/x-www-form-urlencoded, multipart/form-data oder text/plain) sowie eine Reihe sicherer Standard-Header. Diese Anfragen werden gesendet, der Server verarbeitet sie, die Antwort kommt zurück, der Browser liefert sie nur nicht an JavaScript aus. Stymaar beschreibt die »nicht-einfachen Anfragen« — PUT, DELETE, PATCH, POST mit Content-Type application/json, Anfragen mit Authorization-Header usw. Diese Anfragen lösen zunächst einen OPTIONS-Preflight aus; scheitert der Preflight, wird die eigentliche Anfrage nie gesendet.
Das Ingenieursurteil lautet: Jedes Lager hat in seinem eigenen Kontext recht, aber jedes hat seine kontextuelle Wahrheit zur universellen Wahrheit erhoben. Muvlons Satz »the requests happen in any case« ist als universelle Aussage falsch — für nicht-einfache Anfragen verhindert das Preflight-Scheitern sehr wohl das Senden der Anfrage. Stymaars Verteidigung des Originalartikels über den Preflight-Mechanismus hat ebenfalls Lücken — er übersieht, dass das Zoom-Szenario einen lokalen localhost-Server betrifft, die Angriffsfläche von einfachen GET-Anfragen stammt und die Formulierung des Originalartikels »only Javascript running on the zoom.us domain can talk to the localhost webserver« tatsächlich ungenau ist: Jede Website kann mit diesem localhost-Server »sprechen« (einfache Anfragen senden), nur autorisierte Websites können die Antwort lesen. Wenn der localhost-Server gefährliche Operationen auf GET-Endpunkten bereitstellt, verhindert Access-Control-Allow-Origin nicht das Eintreffen der Anfrage, sondern nur das Lesen der Antwort. Und eine destruktive GET-Anfrage ist, einmal gesendet, gesendet.
Die Infinitesimalrechnung des Preflights
Der Preflight-Mechanismus selbst birgt weitere, leicht zu übersehende Details. In der HN-Diskussion wies jemand darauf hin, dass eine POST-Anfrage mit Content-Type text/plain den Preflight umgeht — weil text/plain auf der Whitelist der »einfachen Anfragen« steht. Ein Angreifer kann ein solches Formular konstruieren:
<form action="https://opfer.com/api" method="POST" enctype="text/plain">
<input name='{"schlüssel":"wert", "ignore":"' value='"}'>
</form>
Der an den Server gesendete Inhalt wird zu {"schlüssel":"wert", "ignore":"="}, was wie fehlerhaftes JSON aussieht. Aber wenn das Backend den Content-Type-Header nicht streng prüft, bevor es JSON.parse auf den Body anwendet, kann diese Anfrage die Preflight-Barriere durchdringen. Ein Nutzer, der behauptete, diese Technik bei mehreren Penetrationstests erfolgreich eingesetzt zu haben, meldete sich in den Kommentaren zu Wort. Das ist keine reine Theoriespekulation — wenn der Server keine Content-Type-Validierung vornimmt, kann ein einfacher POST in text/plain oder multipart/form-data eine beliebige Nutzlast transportieren. Ähnlich kann ein Header-Wert wie multipart/form-data; boundary=application/json die Prüfung umgehen, wenn diese nur als Präfix-Match statt als exakter Match durchgeführt wird.
Diese Randfälle illustrieren eine Tatsache: Das CORS-Sicherheitsmodell lässt sich weder auf »Kann die Anfrage den Server erreichen?« noch auf »Kann die Antwort gelesen werden?« reduzieren — es ist ein verzweigter Entscheidungsbaum, in dem einfache und nicht-einfache Anfragen unterschiedliche Pfade nehmen und die Schutzgrenzen auf jedem Pfad unterschiedlich sind. Die Regel eines bestimmten Pfades zur universellen Regel zu verallgemeinern, erzeugt unweigerlich einen kognitiven Bias. Und dieser Entscheidungsbaum verzweigt sich ständig weiter — Sec-Fetch-*-Header, das SameSite-Cookie-Attribut, Cross-Origin-Embedder-Policy, Cross-Origin-Opener-Policy: Jede Schicht fügt neue Semantik über CORS hinzu und macht ein ohnehin schon komplexes mentales Modell noch schwieriger zu beherrschen.
Warum selbst Erklärartikel-Autoren sich irren
Chris Fosters Originalartikel vom Juli 2019 dreht sich um die Schwachstelle des lokalen Zoom-Webservers. Zoom betrieb auf dem Rechner des Nutzers einen Webserver, der auf localhost:19421 lauschte: Wenn ein Nutzer auf einen Zoom-Link klickte, sendete die Webseite eine Anfrage an diesen lokalen Server, um den nativen Client zu öffnen. Um CORS zu umgehen, verwendete Zoom kein AJAX, sondern lud ein Bild und kodierte den Statuscode in den Bildabmessungen. Fosters Empfehlung: Dieser lokale Webserver sollte Access-Control-Allow-Origin: https://zoom.us setzen, sodass »nur JavaScript, das auf der zoom.us-Domain läuft, mit dem lokalen Server kommunizieren kann«.
Die erste Hälfte von Fosters Urteil (Zooms Ansatz ist unsicher) ist korrekt, aber die zweite Hälfte seiner Formulierung (»nur JavaScript von zoom.us kann kommunizieren«) ist technisch mehrdeutig. Genau genommen kann Access-Control-Allow-Origin andere Websites nicht daran hindern, einfache Anfragen an localhost zu senden; es kann nur verhindern, dass das JavaScript anderer Websites die Antwort liest. Wenn der lokale Webserver sensible Operationen auf GET-Endpunkten bereitstellt, reicht der CORS-Header allein nicht aus.
Aber Foster ist nicht der Einzige, der sich irrt. Der gesamte HN-Kommentarthread widmet sich dem Erörtern von CORS auf verschiedene Weise, und die Meinungsverschiedenheiten zwischen den Kommentatoren sind nicht geringer als ihre Meinungsverschiedenheiten mit Foster. Einer beharrt darauf, dass CORS überhaupt keine Anfragen blockiert; ein anderer widerspricht, dass der Preflight genau dafür da ist, Anfragen zu blockieren; ein Dritter springt ein und weist auf das Problem der Preflight-Umgehung durch POST text/plain und Formulare hin; ein Vierter ergänzt, dass selbst wenn die Anfrage gesendet wird, ohne CORS-Header die Antwort nicht lesbar ist und der Schutz für GET-Operationen daher vollständig ist — solange der Server keine Schreiboperationen auf GET bereitstellt. Jede Widerlegungsschicht deckt die Unvollständigkeit der vorherigen Schicht auf, und das Endergebnis ist, dass auch nach zweihundertfünfzig Kommentaren kein Konsens erzielt wurde.
Ich beobachte ein Muster: Die kognitive Schwierigkeit von CORS rührt nicht nur von seiner Komplexität her, sondern davon, dass es vom Entwickler verlangt, drei Dinge gleichzeitig zu verstehen, um das System korrekt zu modellieren — den SOP-Sockel des Browsers, CORS als Lockerungsmechanismus der SOP und die Sicherheits- und Idempotenzkonventionen der HTTP-Methoden. Diese drei Dinge gehören zu drei verschiedenen Bereichen: Browser-Architektur, Web-Sicherheitsprotokolle und RESTful-Design — die meisten Entwickler beherrschen nur ein oder zwei davon. Wer nur mit »SOP + CORS« modelliert, gelangt leicht zu dem Schluss, dass »die Anfrage blockiert wurde« (weil der Gesamteffekt auf Browser-Ebene so aussieht). Wer nur mit »HTTP-Semantik« modelliert, sieht, dass der Server die Anfrage empfangen und eine Antwort zurückgegeben hat — »die Anfrage wurde sehr wohl gesendet«. Beide Modellierungen sind auf ihrer jeweiligen Ebene korrekt, aber ihre Projektion auf denselben Begriff »CORS« erzeugt einen Konflikt.
Der Generationenriss
Eine Beobachtung in den Kommentaren ist besonders interessant: Es könnte sich um ein Generationenproblem handeln. Wenn Sie mit der Webentwicklung begannen, bevor CORS existierte, haben Sie die Zeit erlebt, in der es nur SOP und keine legitimen Cross-Origin-Anfragen gab. Sie wissen, wie JSONP zusammengehackt wurde, Sie wissen, warum <img>- und <script>-Tags Cross-Origin können, XHR aber nicht. Als CORS auftauchte, sahen Sie, wie sich eine Tür in der SOP öffnete — es war eine Lösung. Wenn Sie aber erst nach der Existenz von CORS mit der Webentwicklung begannen, lautete der erste Cross-Origin-Fehler, dem Sie begegneten, »blocked by CORS«, und Ihr Instinkt sagte Ihnen, dass CORS Sie bei der Arbeit behindert — es war ein Problem.
Der Generationenunterschied existiert tatsächlich, aber das tiefergehende Problem ist, dass die Dokumentation, Lehre und Fehlermeldungen von CORS bereits im Design eine kognitive Schlagseite aufweisen. Die Fehlermeldung der Browser-Konsole lautet »blocked by CORS«, nicht »blocked by Same-Origin Policy due to missing CORS authorization«. Die MDN-Dokumentation erklärt den vollständigen Mechanismus, aber die meisten Entwickler lesen nicht die vollständige Dokumentation — sie hören bei der ersten Stack-Overflow-Antwort auf, die ihr Problem behebt. In den HN-Kommentaren gab mehr als eine Person zu: »Jedes Mal, wenn ich auf ein CORS-Problem stoße, muss ich es komplett neu lernen, und dann vergesse ich es wieder.« Ein Kommentator, der sich als CTO bezeichnete, sagte, dass die Nutzer seines Unternehmens massiv auf CORS-Probleme stoßen und Unterstützung suchen, und seine Beobachtung lautet: Es ist heute nicht mehr nötig, CORS wirklich zu verstehen, denn Claude und GPT können CORS-Fehler inzwischen reparieren — man wirft den Fehler einfach dem LLM hin. Ein anderer widersprach sofort: Der letzte CORS-Fehler, auf den er stieß, durchdrang drei Verteidigungslinien — Claude, Copilot und einen Senior-Ingenieur — bevor er gelöst wurde. Wenn schon die Autoren von Erklärartikeln und ihre Leser untereinander streiten, wie zuverlässig können dann die Antworten sein, die LLMs aus chaotischen Trainingsdaten lernen?
Eine Schuld, die nicht beglichen wird
Die Designer von CORS standen vor einer nahezu unmöglichen Aufgabe: Einen sicheren Autorisierungsmechanismus für Cross-Origin-Interaktionen des Browsers bereitzustellen und gleichzeitig mit zwanzig Jahren Web-Altlasten kompatibel zu bleiben. Die Cross-Origin-Fähigkeit von HTML-<form> existierte bereits über zwanzig Jahre vor dem Aufkommen von CORS; sie einfach abzuschalten, hätte das gesamte Internet zerbrochen. CORS wählte einen Mittelweg: Rückwärtskompatibilität für »einfache Anfragen« beibehalten, für »nicht-einfache Anfragen« den Preflight einführen. Diese Wahl war damals pragmatisch, aber sie internalisierte die Komplexität im Protokoll selbst — Entwickler müssen verstehen, welche Anfragen einfach sind und welche nicht, welche Header sicher sind und welche nicht, warum eine OPTIONS-Anfrage erscheint und in welcher Beziehung sie zur eigentlichen Anfrage steht. Zwanzig Jahre später kamen neue Schichten obendrauf — SameSite, Sec-Fetch, COEP, COOP — und die Komplexität hat nur zugenommen.
Ich neige zu der Ansicht, dass CORS’ kognitive Schuld in der Evolutionsweise der Web-Plattform selbst wurzelt — Rückwärtskompatibilität ist eine harte Nebenbedingung, schrittweise Evolution ist der einzig gangbare Weg, und die Kompromisse jeder Phase hinterlassen konzeptionelle Schulden, die nachfolgende Entwickler zusätzlich erlernen müssen. Diese Schuld wird kaum zu tilgen sein, denn sie ist in die DNA der Browser und Milliarden von Webseiten eingraviert.
Der HN-Kommentarthread wird wohl nicht der Endpunkt des CORS-Problems sein. Aber er bietet einen wertvollen Querschnitt — er zeigt, dass selbst diejenigen in der technischen Gemeinschaft, die sich am meisten für dieses Thema interessieren, nach zwei Tagen intensiver Diskussion immer noch nicht in der Lage sind, sich auf die grundlegendsten Fakten zu einigen. Wenn diese Gruppe sich nicht einigen kann, ist die Erwartung, dass der durchschnittliche Entwickler jedes Detail von CORS präzise beherrscht, wahrscheinlich unrealistisch.
Dieser Artikel basiert auf der technischen Analyse von Chris Fosters Originalartikel und dem Hacker-News-Diskussionsthread. Der Autor ist weder Originalautor der CORS-Spezifikation noch Browser-Engine-Entwickler; die hier präsentierte Interpretation der technischen Mechanismen stammt aus dem Lesen und Verstehen öffentlicher Standardisierungsdokumente und Community-Diskussionen und kann Ungenauigkeiten enthalten. Sollten Sie technische Fehler in diesem Artikel entdecken, orientieren Sie sich bitte am WHATWG Fetch Standard und den MDN Web Docs als maßgeblichen Quellen.