Entwickler verstehen CORS nicht (2019)
(fosterelli.co)- Die Schwachstelle im lokalen Webserver von Zoom zeigte, wie leicht Sicherheitsgrenzen zusammenbrechen können, wenn viele Webentwickler die Funktionsweise von CORS missverstehen
- Bei der Kommunikation mit dem lokalen Server
localhost:19421übermittelte Zoom Statuscodes über Bildgrößen statt per AJAX; das lässt sich als Umgehung von CORS interpretieren - Chrome wendet CORS-Header auch auf localhost-Webserver an, und die Kommunikation zwischen Frontend und Backend auf unterschiedlichen localhost-Ports wird im Browser ebenfalls unterstützt
- Ein sichereres Design wäre, wenn der lokale Server eine REST API bereitstellt und
Access-Control-Allow-Originso setzt, dass nur JavaScript von zoom.us darauf zugreifen kann - Wer die Same-Origin-Policy umgeht, kann den Code zwar zum Laufen bringen, legt damit aber privilegierte Funktionen des lokalen Servers für alle Websites im Internet offen
Die CORS-Umgehung durch den lokalen Zoom-Webserver
- Bei der Full-Stack-Beratung in Unternehmen unterschiedlichster Größen und Branchen zeigte sich immer wieder, dass Webentwickler CORS nicht richtig verstehen
- In der jüngsten Zoom-Schwachstelle entdeckte der Sicherheitsforscher Jonathan Leitschuh, dass Zoom auf dem Rechner des Nutzers einen Webserver unter
http://localhost:19421startet- Öffnet ein Nutzer einen Zoom-Link, sendet die Zoom-Website eine Anfrage an den localhost-Webserver, um die native Zoom-App zu starten
- Statt gewöhnlicher AJAX-Anfragen lud der lokale Zoom-Webserver ein Bild und kodierte Fehler- bzw. Statuscodes über unterschiedliche Bildgrößen
- Die Annahme, Browser würden die CORS-Richtlinie von localhost-Servern ignorieren, ist falsch; Chrome beachtet CORS-Header auch bei localhost-Webservern
- Auch wenn ein Create-React-App-Frontend und eine Backend-API auf unterschiedlichen localhost-Ports laufen, entstehen Cross-Origin-Requests, und das wird von allen Browsern unterstützt
- Nachdem AJAX-Anfragen blockiert wurden, scheint Zoom CORS mit einem Bild-Hack umgangen zu haben
- Dadurch konnten nicht nur die Zoom-Website, sondern auch andere Websites im Internet Aktionen des nativen Clients auslösen und auf Antworten zugreifen
Sichere Alternative und verbleibende UX-Probleme
- Eine sichere Umsetzung wäre, wenn der Webserver auf
localhost:19421eine REST API implementiert und den HeaderAccess-Control-Allow-Originaufhttps://zoom.ussetzt- Dann könnte nur JavaScript, das auf der Domain zoom.us ausgeführt wird, mit dem localhost-Webserver kommunizieren
- zoom.us könnte zusätzlich einen Content-Security-Policy-Header setzen, der das Rendern in iframes blockiert, damit Zoom-Meetings nicht im Hintergrund automatisch geöffnet werden
- Das Problem, dass jede Seite den Browser weiterhin auf einen zoom.us-Meeting-Link umleiten kann, bleibt bestehen
- Das ist allerdings eher eine Frage der von Zoom gewählten User Experience als eine Software-Schwachstelle
- Zoom verletzt damit die Erwartung von Nutzern, dass Kamera und Mikrofon nicht plötzlich für Fremde aktiviert werden, nur weil sie auf einen Link klicken
- Wenn man Browser-Standard-Popups aus UX-Gründen vermeiden will, könnte die App stattdessen ein eigenes Popup anzeigen; Google Meet macht das gut vor
- Schon das Ausführen eines Webservers auf localhost ist ein riskanter Ansatz, und insbesondere sollten privilegierte Funktionen wie Software-Installation nicht allen Websites im Internet zugänglich gemacht werden
- CORS ist genau für solche Szenarien gedacht und sollte daher nicht umgangen werden
CORS-Verwirrung ist kein Zoom-spezifischer Fehler
- Ob Zoom diesen Ansatz wirklich gewählt hat, weil man CORS nicht verstanden hat, ist nicht sicher
- lerunicorn auf Reddit vermutet, dass Firefox XHR von sicheren zu unsicheren Ursprüngen blockieren könnte
- Firefox unterstützt dies jedoch, wenn der Origin localhost ist
- Native Apps können eigene selbstsignierte Zertifikate erzeugen und auch Browser-Erweiterungen verwenden
- In keinem Fall ist das ein legitimer Grund, auf Origin-Filterung zu verzichten
- CORS-Verwirrung ist kein Problem nur bei Zoom
- Auf Stack Overflow gibt es zahlreiche Fragen zu
Access-Control-Allow-Origin - Manche Express-Beispiele empfehlen unsichere Standardwerte, die eine Anwendung verwundbar machen können, wenn man sie einfach kopiert
- Auch andere Anbieter hatten bereits dieselbe Schwachstelle wie Zoom
- Auf Stack Overflow gibt es zahlreiche Fragen zu
- Entwickler wollen ihren Code zum Laufen bringen, aber wer die Same-Origin-Policy komplett umgeht, legt wie im Fall Zoom lokale Berechtigungen für externe Websites offen
- CORS-Verwirrung tritt sowohl bei erfahrenen als auch bei neuen Entwicklern auf; ob die CORS-API zu komplex ist oder ob Schulung zu CORS und CSP fehlt, ist unklar, aber der aktuelle Ansatz funktioniert offensichtlich nicht gut
1 Kommentare
Hacker-News-Kommentare
Offenbar hat auch der TFA CORS nicht richtig verstanden oder es stark falsch erklärt.
Access-Control-Allow-Origin: https://zoom.usstellt nicht sicher, dass nur JavaScript von der Domain zoom.us mit dem localhost-Server kommunizieren kann. JavaScript von anderen Websites kann Anfragen anlocalhost:19421genauso senden. CORS schränkt nichts ein, sondern lockert eine bestehende Einschränkung. Dieser Header erlaubt lediglich JavaScript, das auf zoom.us ausgeführt wird, die Antwort vonlocalhost:19421zu lesen; die Anfrage selbst findet ohnehin statt, daher muss das Backend so gebaut sein, dass keine Seiteneffekte entstehen.GET-Anfragen werden zwar gesendet, sollten aber per Definition idempotent sein, sodass sie bei korrekt implementiertem Server keine Seiteneffekte verursachen können; bei GET ist entscheidend, ob die Antwort gelesen werden kann. Nicht-idempotente Anfragen, die Seiteneffekte haben können, schicken in Cross-Origin-Situationen dagegen zunächst eine preflight-OPTIONS-Anfrage statt der eigentlichen Anfrage; fehlen in der OPTIONS-Antwort die richtigen Header, wird die eigentliche Anfrage nicht gesendet.
Missverständnisse über CORS sind so weit verbreitet, und die Dokumentationen widersprechen sich oft, dass man kaum erwarten kann, dass eine unbekannte Gegenstelle es korrekt implementiert hat. Wenn ein Protokoll so viel Verwirrung stiftet, weiß man selbst dann nicht, ob die andere Seite korrekt funktioniert, wenn die eigene es tut. Wenn Leute ihren Code so lange angepasst haben, bis er mit anderen Implementierungen funktioniert, verschwimmt auch, ob der Fehler bei ihnen selbst oder bei der Gegenseite liegt.
Zum Beispiel kann ein POST mit
Content-Typetext/jsonnicht ohne OPTIONS-Preflight an einen Drittanbieter-Host gesendet werden, ein POST mitmultipart/form-datahingegen schon, und CORS blockiert das nicht. Wenn der Endpunkt dannContent-Typenicht strikt prüft und einfach JSON annimmt, kann jede Website ohne Interaktion des Nutzers POST-Anfragen senden.Ein ordentlicher Webentwickler sollte GET/HEAD/OPTIONS nicht den Zustand ändern lassen, und an einer Besprechung teilzunehmen ist eine Zustandsänderung. PUT/DELETE sollten ebenfalls idempotent sein. Eine POST-API mit einem anderen Format als JSON oder Formularen sollte den
Content-Type-Header prüfen, und beiPUT/PATCH/DELETEsowie POSTs mit einem nicht formularbasiertenContent-Typewird ein Preflight ausgelöst, sodass CORS geprüft wird, bevor die eigentliche Anfrage den Server erreicht.Es reicht nicht, nur ein Zertifikat zu erstellen; es muss als Root-CA-Zertifikat in allen Browser-Trust-Stores der Maschine installiert werden. Wenn der private Schlüssel der Root-CA nicht sauber geschützt wird, kann jede Website einen Man-in-the-Middle-Angriff durchführen, daher sind zumindest Namensbeschränkungen nötig (https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10). In Chrome funktionierte das bei Root-CAs jedoch bis v112 im Jahr 2023 nicht (https://alexsci.com/blog/name-non-constraint/), weshalb man eine Intermediate-CA hinzufügen und dort die Einschränkungen setzen musste. Natürlich sollte man den Root-CA-Schlüssel danach verwerfen.
Ich habe früher einmal bei einem Projekt mit lokaler Root-CA Basic Constraints hinzugefügt, sie aber fälschlicherweise in die Root-CA gesetzt und nicht in allen Browsern getestet.
Ich wünschte, mehr Leute würden die CORS-Dokumentation von MDN lesen. Sie hat mir sehr geholfen, als ich CORS verstehen wollte, und ich hätte nicht gedacht, dass die Leute in den Kommentaren sich damit so schwertun.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
Schwer zu verstehen ist nicht nur CORS, sondern auch, dass viele Entwickler das Bedrohungsmodell nicht richtig verstehen.
Selbst nach einer Erklärung ist oft nicht klar, warum das ein großes Problem ist. Gerade Backend-Entwickler konfigurieren häufig CORS, aber aus ihrer Sicht wirkt es nicht besonders wichtig, weil CORS kein Mechanismus zum Schutz von Zugriffsrechten ist. Für Angreifer scheint es, als könnten sie nichts mitnehmen, und aus Frontend-Sicht wirkt es leicht wie ein lästiges Hindernis. Dieser Artikel zeigt gute konkrete Beispiele.
Als Betriebsverantwortlicher habe ich es dann am Load Balancer noch einmal korrekt eingerichtet, und zumindest funktioniert die Anwendung jetzt. CORS ist schwer zu verstehen, aber noch bedauerlicher ist, dass viele Entwickler nicht nur das Bedrohungsmodell hinter CORS, sondern die Webentwicklung insgesamt, insbesondere das HTTP-Protokoll, nicht verstehen.
multipart/form-dataist okay, aber Anwendungs-JavaScript nicht.CORS ist optional, und andere Bibliotheken oder Tools können es einfach ignorieren. CORS ist eigentlich nur sinnvoll, um XSS und CSRF bei tatsächlich eingeloggten menschlichen Nutzern zu verhindern; für andere Angriffsszenarien ist es bedeutungslos, weil dort ohnehin Skripte oder Programme verwendet werden, die HTTP-Header fälschen. Deshalb schalten Leute am Ende alle CORS-Optionen ein, was im schlimmsten Fall XSS und CSRF zulässt.
Dieser Kommentarbereich wirkt wirklich unerquicklich auf niedrigem Niveau und bestätigt eher genau den Punkt des Autors
Wer schon vor CORS Webentwicklung gemacht hat, versteht, dass Cross-Domain-Requests ursprünglich verboten waren und CORS eingeführt wurde, um diese Beschränkung sicher zu umgehen. Deshalb liegt es nahe, einfach zu akzeptieren, dass man CORS aktivieren muss, wenn man etwas Bestimmtes erreichen will
Wer dagegen erst nach CORS Webentwicklung gelernt hat, sieht nur den Ablauf: Man versucht einen Cross-Origin-Request, der Browser entscheidet, dass er nicht erlaubt ist, versucht einen CORS-Preflight, und wenn das fehlschlägt, erscheint ein CORS-Fehler in der Konsole. Wenn man die internen Abläufe nicht kennt und die Dokumentation nicht gelesen hat, vermutet man dann, dass CORS die Ursache dafür ist, dass die Anfrage blockiert wird, und versucht, „CORS zu deaktivieren“. Aber CORS ist nicht die Ursache des Problems, sondern die Lösung
Wenn Leute mit demselben Missverständnis das dann in Tutorials und Online-Diskussionen selbstbewusst wiederholen, wird es nur noch verwirrender
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
Beim Lesen der Kommentare war ich erleichtert zu sehen, dass nicht nur ich so denke. Niemand versteht CORS, weil es zu komplex ist und es zu viele Konflikte gibt
Standards und Header ändern sich ständig, daher probieren Entwickler meist einfach Dinge aus, bis es funktioniert, deployen das Produkt und sind fertig. Selbst wenn es läuft, können in der Entwicklerkonsole noch Fehler und Warnungen stehen, aber solange es oberflächlich funktioniert, fasst man es lieber nicht mehr an
Um CORS zu verstehen, muss man zuerst die Same-Origin-Policy verstehen
Vor allem wenn die Frage „Warum braucht man das überhaupt?“ schwerfällt, ist das ein guter Einstieg: https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Same-origin_policy
Ich habe die Same-Origin-Policy früher einmal als Interviewfrage verwendet, aber viele Bewerber waren damit nicht vertraut, sodass man aus dieser Frage nur wenig Information gewinnen konnte
Wer Web-Apps entwickelt hat, sollte irgendwann auf die Same-Origin-Policy gestoßen sein. Wenn nicht, fragt man meist weiter nach Dingen wie der Kommunikation mit dem Backend. Ob jemand schon einmal auf ein CORS-Problem gestoßen ist, aber nur den schnellsten Workaround angewendet und es dann vergessen hat, oder ob er wirklich versucht hat, es zu verstehen, ist für manche Rollen ein nützliches Signal
Für Backend-Rollen ist die Frage weniger geeignet. Nicht jeder Backend-Entwickler hat eng mit Frontend-Teams gearbeitet, die häufig auf CORS-Probleme stoßen
Was mir zu CORS in Erinnerung geblieben ist: Debugging dauert viel länger als erwartet, die Browser-Fehlermeldungen sind absichtlich dürftig, und CORS-Fehler lassen sich anfangs nur schwer von anderen Fehlermodi unterscheiden
Wenn der Server natürlich CORS-Requests nicht versteht und merkwürdige Antworten zurückgibt, kann das am Ende trotzdem als CORS-Fehlschlag erscheinen
Der Kommentarbereich ist ziemlich unterhaltsam, daher noch ein Nachtrag: Die Same-Origin-Policy schützt davor, dass der Browser Informationen an Websites ohne Zugriffsrechte preisgibt, und CORS erlaubt es, diesen Schutz abzuschwächen
Zum Beispiel verhindert die Same-Origin-Policy, dass
example.comdie Abonnementliste vonyoutube.comabrufen kann. Mit CORS kann man aber erlauben, dassexample.comaufyoutube.com/public/*zugreiftEin weiterer Einsatzzweck ist, dass ein Backend-API nicht einfach unter einem anderen Frontend betrieben werden kann und dadurch Datenabfluss entsteht. Es verhindert etwa Situationen, in denen man zwar beim echten Dienst eingeloggt ist, sich aber auf
g00gle.combefindet und alle Anfragen per Man-in-the-Middle manipuliert werden könntenIch bin auch einer von denen. CORS ist ein Thema, das ich regelmäßig neu lernen muss, und ich vergesse es immer wieder, sodass es nie richtig hängen bleibt
Vermutlich liegt es daran, dass ich Backend-Entwickler bin und fast nie mit CORS-Problemen zu tun habe. Dinge, die ich nicht täglich benutze, vergesse ich leicht
In einer normalen Welt würde die Fehlermeldung Hinweise wie „Response-Header“ oder „meta-Tag“ enthalten, aber es wirkt, als hätten die großen Browserhersteller Leute eingestellt, die rätselhafte Meldungen schreiben. Das „requested resource“ in Chrome ist noch das Beste, aber immer noch kryptisch
Bessere Meldungen wären zum Beispiel, dass die Ressource von
https://bank.comkeine CORS-Header hat und deshalb keine Cross-Origin-Requests erlaubt, oder dass der aktuelle Ursprung nicht auf der CORS-Allowlist steht. Dazu sollte auch der Preflight-Request im Netzwerk-Tab und ein MDN-Link angezeigt werden. Bei CSP wäre es ebenfalls besser, klar zu sagen, dass diese Seite die Ressource wegen des CSP-Headers auf dieser Seite nicht laden kann, und direkt zu den Request-Headern der Seite oder zummeta-Tag im Inspector zu verlinkenLetztlich beruht das meist auf der Annahme, dass der Server nur über unveränderte Browser-Requests angesprochen wird. Die Zoom-Sicherheitslücke entstand auch deshalb, weil sich CORS und CSP clientseitig zu leicht umgehen lassen. Zoom war dabei sicher schlecht, faul und dumm, aber ich finde, auch die Community trägt Verantwortung, wenn sie dieses Modell weiter aufrechterhält
Ich verstehe, wie die Same-Origin-Policy verhindert, dass der Browser bösartige Skripte ausführt und Informationen exfiltriert. Ich verstehe auch, dass ein Server mit dem Header
Access-Control-Allow-Originerklärt, dass er zusätzlichen Origins vertraut und damit die SOP lockertTrotzdem verstehe ich den Zweck des Headers
Access-Control-Allow-Headersnoch nicht. Er scheint weder die Browsersicherheit zu verbessern noch erst recht die Serversicherheit. Ich frage mich, ob Protokolldesigner ihn der „Vollständigkeit halber“ aufgenommen haben. Dazu passend: https://stackoverflow.com/questions/17992042