Cross-Origin Resource Sharing (CORS)
Webapplikationen sind aus unserem digitalen Alltag fast nicht mehr wegzudenken. Ob man ihnen bei der Zeiterfassung, beim Online Shopping oder durch Soziale Medien begegnet, früher oder später läuft man ihnen über den Weg. Auch der Aufgabenbereich von Webapplikationen hat sich über die Jahre mehr und mehr ausgeweitet. Schon lange werden durch Webapplicationen nicht nur statische HTML-Seiten aufgerufen, sondern es sind ganze Frameworks im Einsatz, um unsere Daten zu verarbeiten und zu speichern. Deshalb sind sie auch ein beliebtes Ziel für Hacker, denn bei einem Grossteil von Schwachstellen handelt es sich um ganz banale Miskonfigurationen, die ausgenutzt werden können.
Alle Paar Jahre veröffentlicht die OWASP Foundation [1], welche sich für mehr Sicherheit in Webapplikationen einsetzt, ihre “Top Ten” Liste der kritischsten und meist verbreitetsten Sicherheitslücken in Webapplikationen [2]. Das oben erwähnte Problem der Miskonfiguration befindet sich auf Platz 5 und wurde in der Ausarbeitung der Liste im Jahr 2021 in 4% der heutigen Webapplikationen gefunden.
| Rang | Security Risk |
|---|---|
| 1. | Broken Access Control |
| 2. | Cryptographic Failures |
| 3. | Injection |
| 4. | Insecure Design |
| 5. | Security Misconfiguration |
| 6. | Vulnerable and Outdated Components |
| 7. | Identification and Authentification Failures |
| 8. | Software and Data Integrity Failures |
| 9. | Security Logging and Monitoring Failures |
| 10. | Server-Side Request Forgery |
Natürlich setzt sich dieser Punkt aus sehr vielen verschiedenen Miskonfigurationen zusammen, aber in diesem Blog möchte ich spezifisch auf CORS-Miskonfigurationen eingehen. Worum handelt es sich bei CORS genau? Welche Miskonfigurationen können potenziell entstehen und welche Auswirkungen haben diese auf die Sicherheit einer Webapplikation?
Die meisten Webentwickler kennen CORS nur als einen mühsamen Fehler, welcher verhindert, dass Bilder und andere Ressourcen im Browser nicht richtig angezeigt werden. CORS kann auch ein Spielverderber sein, wenn man von der eigenen Webapplikation auf wichtige APIs zugreifen will. Aus eigener Erfahrung kann dies besonders mühsam sein, wenn wir für unsere goAware Phishing-Simulationen die Ressourcen (wie Bilder, Fonts oder CSS-Styles) einer Webseite nicht dynamisch für unsere Phishing Seiten “ausleihen” können.
Ein kleines Beispiel: Nehmen wir an, ich möchte von meiner lokal gehosteten Webapplikation auf Port 5500 auf meine eigene API zugreifen. Diese API stelle ich über Port 3000 ebenfalls lokal auf meiner Maschine zur Verfügung. Ohne weitere Konfiguration werde ich prompt blockiert und kann meine eigene API nicht testen. In der Console meines Webbrowsers werde ich mit folgender Mitteilung konfrontiert:
Solche Fehlermeldungen haben auch mir in der Vergangenheit bereits viele Kopfschmerzen bereitet. In der Fehlermeldung ist von “CORS Header” und “Same Origin Policy” die Rede. Um CORS zu verstehen, müssen wir uns also zuerst mit der “Same Origin Policy” befassen, welche hier die Rolle des Spielverderbers einnimmt.
1. Was ist die Same Origin Policy (SOP)?
Die Same Origin Policy wurde im Jahr 1995 zu Zeiten des Netscape-Browsers erschaffen. Diese Policy war eine Reaktion auf eine Sicherheitsschwachstelle, bei der Webseiten in einem Browser auf die lokalen Daten auf dem Computer des Users aber auch auf die Daten von anderen Webseiten zugreifen konnten, wenn diese gleichzeitig in unterschiedlichen Frames im Browser geladen wurden. Die Idee der Same Origin Policy war es also zu verhindern, dass fremde Domänen auf die Ressourcen (und somit auch auf die vertraulichen Daten) anderer Domänen zugreifen können. Ressourcen dürfen nur von der eigenen, also Domänen mit dem gleichen Origin, aufgerufen werden. Diese Policy ist standardmässig seit 1995 in allen Browsern integriert.
Wenn man als Benutzer also beispielweise gleichzeitig auf einer Social Media Platform und dem E-Banking angemeldet ist, kann nur die Domäne der Bank selbst auf deren Ressourcen zugreifen.
Same Origin Policy im Einsatz
|
Dies ist grundsätzlich eine sehr einfache Lösung um sicherzustellen, dass der Server keine sensiblen oder persönlichen Daten (zum Beispiel den aktuellen Kontostand) an andere Domänen schickt, welche diese Informationen nicht erhalten sollten. Die Same Origin Policy ist aber sehr restriktiv. Dies wird spätestens dann klar, wenn wir uns wieder dem Beispiel der lokal gehosteten Webapplikation widmen.
In dem Beispiel möchten wir vom Frontend (lokal gehostet auf Port 5500) auf unsere API zugreifen (lokal gehostet auf Port 3000). Als praktisches Beispiel für das Frontend erstellen wir eine sehr simple Client-Webseite. Diese enthält nur einen einfachen Button “Click me!”, welcher bei einem Klick eine Anfrage an unsere API (http://localhost:3000/api) sendet. Im Codebeispiel unten wird bei einem Klick also die Funktion makeApiRequest() auf Zeile 5 ausgeführt. Diese Funktion zeigt bei einem erfolgreichen API-Call die Nachricht des Servers auf der Seite an (Zeile 10-14). Zur Veranschaulichung der Beispiele werden auch die Response-Header auf der Webseite angezeigt. Bei einem Fehler wird eine Fehlermeldung angezeigt (Zeile 16-17).
Unser API Backend wiederum besteht aus einem einfachen Node.js Endpoint. Die API kann über die URL http://localhost:3000/api aufgerufen werden. Wird diese aufgerufen, geben wir als Antwort die Meldung “A successfull API call!” zurück um zu bestätigen, dass der Aufruf funktioniert hat. In einem Browser kann dies direkt ohne Einschränkung durch die Same Origin Policy getestet werden. Denn diese greifen im Browser selbst nicht und sind nur für die Kommunikation zwischen Webapplikationen gedacht.
Wenn wir unsere zwei Applikationen starten und den Button im Frontend anklicken, bekommen wir nun aber eine Fehlermeldung und den bekannten Fehler in der Console. Denn der Aufruf der API wird nun nicht durch den Browser selbst getätigt, sondernn durch die Webapplikation auf Port 5500, wodurch die Single Origin Policy zum Zug kommt.
Unter der Regel der Same Origin Policy sind unsere zwei Applikationen nämlich unterschiedliche Origins, weshalb die Abfrage untersagt wird.
|
Ob es sich bei zwei URIs um den gleichen Origin handelt oder nicht wird durch 3 Faktoren bestimmt:
-
Haben die beiden URIs das gleiche Schema (beide HTTP oder HTTPS)?
-
Haben die beiden URIs die gleiche Domäne (inklusive gleiche Subdomänen)?
-
Haben die beiden URIs den gleichen Port? Wird kein Port explizit angegeben, gilt der standardmässige Port für das Schema, also 80 oder 443.
In unserem Beispiel scheitern wir also im Punkt 3, da die beiden Applikationen auf unterschiedlichen Ports gehostet werden. Wie kann man dieses Problem nun umgehen? Besonders für moderne Webapplikationen ist es immerhin wichtig, dass sie unter Umständen Anfragen an andere Webapplikationen im Namen des Users senden können (zum Beispiel um eine authorisierte Zahlung durchzuführen). Hier kommt nun das Cross-Origin Resource-Sharing (CORS) zum Einsatz.
2. Was ist Cross-Origin Resource Sharing (CORS)?
Eine Möglichkeit die oben genannte Single Origin Policy sicher zu umgehen, ist die Implementation von CORS. Sie erlaubt es serverseitig bestimmte Origins zu konfigurieren, welche die Single Origin Policy umgehen dürfen und somit die Ressourcen des Servers laden dürfen. Oft werden diese von Webapplikationen aber zu locker definiert, um sicherzustellen, dass die Funktionalität der Webseite auf alle Fälle gewährleistet ist. Dadurch können sich unerwünschte Schwachstellen einfach einschleichen. Die Konfiguration solcher Origins wird serverseitig mittels dem. Access-Control-Allow-Origin Access-Control-Allow-Origin Header gemacht. Eine Webapplikation kann also konfigurieren, welchen Origins sie explizit vertraut und welche ihre öffentlichen Ressourcen im Browser laden dürfen. Diese Konfigurationen werden über die HTTP-Response-Header an den Client kommuniziert. Wir können also unsere API so konfigurieren, dass sie diesen Header für unser Frontend folgendermassen festlegt:
|
In unserem Beispiel-Code kann dies zum Beispiel folgendermassen implementiert werden. Die genaue Implementation hängt natürlich vom jeweiligen Framework der Webapplikation ab. Durch die Verwendung des npm-Moduls cors (Zeile 2 und Zeile 6) können wir sämtlichen Domänen erlauben auf unsere API zuzugreifen. In der Praxis wird der Server dann den Header Access-Control-Allow-Origin: * an den Client zurücksenden.
In diesem Fall können wir nun also erfolgreich von unserem Frontend (Port 5500) auf unsere API (Port 3000) zugreifen. Wenn man nun nur dieses Beispiel betrachtet, scheint der Ansatz mit dem Access-Control-Allow-Origin Header eine gute Lösung zu sein. Dies kommt aber daher, dass unser Beispiel etwas zu simpel ist. “Echte” Webapplikationen müssen oft mehreren Subdomänen und externen Domänen vertrauen und ihnen Zugriff auf unterschiedliche Ressourcen geben. Solch eine Konfiguration kann jedoch über einen simplen Header nicht so einfach gesetzt werden.
Der Access-Control-Allow-Origin Header kann nämlich nur auf die folgenden 3 Arten gesetzt werden [3]:
| Gesetzter Header | Beschreibung |
|---|---|
Access-Control-Allow-Origin: *
|
Beliebige Origins dürfen auf die Ressourcen der Applikation zugreifen. Die Übertragung von Credentials wird jedoch bei der Wildcard * nicht erlaubt. |
Access-Control-Allow-Origin: <origin>
|
Ein einzelner Origin kann angegeben werden. Eine Liste von Origins wie Access-Control-Allow-Origin: http://localhost:5500, http://localhost:8080 ist aber beispielsweise nicht möglich! Auch sind keine Wildcards wie Access-Control-Allow-Origin: http://*.localhost:5500 möglich. |
Access-Control-Allow-Origin: null
|
Der Origin null kann auf Ressourcen zugreifen. Diese Einstellung sieht auf den ersten Blick vielleicht sicher aus. Jedoch werden beispielsweise sandboxed iframes mit diesem Origin ausgestattet. Zusätzlich ist es mit dieser Konfiguration (anders als beim Wildcard-Origin *) auch erlaubt Credentials zu übertragen. Diese Konfiguration des Headers sollte also nicht benutzt werden. |
Neben dem Access-Control-Allow-Origin-Header gibt es noch weitere Header, welche mit CORS in Verbindung stehen. Diese möchte ich vollständigkeitshalber erwähnen, aber nicht tiefer darauf eingehen.
| Header | Mögliche Optionen | Hinweis |
|---|---|---|
Access-Control-Allow-Credentials |
true
|
Dürfen Credentials über in Cross-Origin Requests mitgegeben werden.
|
Access-Control-Allow-Headers
|
* / <header-name> / <header-name>, <header-name>, ... |
Gibt eine Liste von Headers zurück, welche bei einem Request erlaubt sind.
|
Access-Control-Request-Methods |
* / <method> / <method>, <method>, ... |
Gibt eine Liste von Methoden (PUT, DELETE, etc.) zurück, welche bei einem Request erlaubt sind. GET, HEAD und POST sind immer erlaubt. |
Access-Control-Expose-Header
|
* / <header-name> / <header-name>, <header-name>, ...
|
Gibt eine Liste von Methoden an, die durch Skripte im Browser bei einem Cross-Origin Request ausgelesen werden können. Standardmässig sind das Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified und Pragma |
Im Unterschied zum Access-Control-Allow-Origin-Header können bei diesen Headern auch kommagetrennte Listen von Headern oder Methodennamen gesetzt werden. Beim Access-Control-Allow-Origin-Header aber können mehrere Origin zusammen nur mittels * freigegeben werden, oder nur ein einzelner Origin mittels <origin>. Diese Einschränkung in der Definition des Access-Control-Allow-Origin-Headers führt dazu, dass viele Entwickler diesen Header nicht einfach statisch setzen können. Ein Origin ist für moderne Webapplikationen oft nicht realistisch und die Einstellung alle Origins zuzulassen, ist oft zu grosszügig. Die einzige Lösung um mehrere Origins freizugeben, ist durch eine dynamische Generierung des Access-Control-Allow-Origin Headers, oft basierend auf dem Origin-Header Input des Users. Dies bedeutet, dass die Webapplikation, welche eine Anfrage erhält, den Origin Header je nach Request (und somit Input des Users) programmatisch setzt. Und genau hier steckt der Teufel im Detail.
3. CORS-Vulnerabilities
Wo liegen also nun genau die konkreten Gefahren bei einem falsch gesetzten oder dynamisch generierten Access-Control-Allow-Origin-Header. Im Folgenden möchte ich einige Varianten veranschaulichen wie dies umgesetzt werden kann und wie diese Implementationen konkret ausgenutzt werden könnten.
3.1. Reflektierter Origin
Die einfachste und kritischste Art um den dynamischen Header serverseitig zu generieren, ist einfach den vom Client angegebenen Origin-Header zurückzugeben.
In unsererm Code Beispiel könnte dies zum Beispiel in der API-Applikation so konfiguriert werden. Dabei wird in den corsOptions auf Zeile 7-9 anstelle eines fixen Origins eine Funktion hinterlegt, was bedeutet, dass der Origin des Clients dynamisch durch diese Funktion evaluiert wird. In diesem Beispiel geben wir durch den zweiten Parameter true im Callback auf Zeile 8 an, dass jeder Origin ohne weitere Prüfung akzeptiert wird.
Dies bedeutet, dass effektiv jede Domäne auf die Ressourcen der Applikation, falls nicht weiter geschützt, zugreifen könnte. Diese Implementation kann von einem Angreifer sehr schnell identifiziert und ausgenutzt werden. Hier ein fiktives Beispiel, wie ein solcher Angriff ablaufen könnte.
|
Aus diesem Beispiel geht hervor wie kritisch eine solche Miskonfiguration unter Umständen sein kann. Zudem kann ein Angreifer sehr einfach herausfinden (meistens in 2-3 Anfragen), ob eine Webseite von dieser Fehlkonfiguration betroffen ist.
3.2. Origin Whitelist
Eine andere naheliegende Implementation um dem Access-Control-Allow-Origin-Header dynamisch zu generieren, ist eine Whitelist zu verwenden. Dabei ist auf dem Server eine Liste von Origins hinterlegt, welche auf die Ressourcen der Applikation zugreifen dürfen. Bei einem Request wird der Wert im Origin-Header mit den Origins in der Whitelist überprüft. Auch in einem solchen Fall können jedoch Fehler auftreten, dies vor allem in der Art wie der Vergleich der Origins implementiert ist.
Für die Vergleiche werden oft sogenannte “Regular Expressions” oder “Regex” benutzt. Diese definieren wie eine erlaubte Domäne geschrieben werden soll. So kann der Regex beispielsweise so konfiguriert sein, dass die erlaubten Origins explizit “vulnshop.com” enthalten müssen. Damit können beispielsweise alle Subdomänen einer Domäne inbegriffen werden.
Eine mögliche Implementation könnten wir in unserer API zum Beispiel folgendermassen implementieren. Für dieses Beispiel verwenden wir die Domain-Namen hacker.com sowie hackervulnshop.com für unser Frontend und möchten von da aus auf die API zugreifen.
|
Um dieses Szenario auszunutzen, kann ein Angreifer aber das gleiche Spiel wie oben beschrieben mit der selbstregistrierten Domäne hackervulnshop.com durchführen. Das gleiche gilt auch für Regex, welche sicherstellen, dass die erlaubten Origins mit “vulnshop.com” beginnen oder enden müssen. In diesem Fall kann beispielsweise die Domäne vulnshop.com.hacker.com die Whitelist umgehen.
3.3. Null Origin
Ein besonders fataler Fehler ist, den null-Origin selbst whitezulisten. Wie bereits erwähnt, werden beispielsweise sandobxed iframes mit diesem Origin ausgestattet. Dies bedeutet, dass jede Webseite durch das Einbauen eines solchen iframes den null-Origin verwenden kann. Das Whitelisten eines null-Origins hat also effektiv den gleichen Effekt wie Access-Control-Allow-Origin: *. Zusätzlich werden aber die Sicherheitsvorkehrungen des * Headers umgangen, da es möglich ist, Credentials mitzugeben.
Fazit
Die Schwachstellen, welche durch CORS-Miskonfigurationen entstehen, können unter Umständen sehr gravierend sein. Die gute Nachricht ist, dass es sich bei diesen Fehlern um reine Miskonfigurationen handelt. Werden die CORS-Header richtig konfiguriert, können diese Schwachstellen somit behoben werden. Auch bei diesen Konfigurationen gilt der Grundsatz “Least Privilege”. Die CORS-Header sollten so weit wir möglich eingeschränkt werden und auf die benötigten Origins reduziert werden. Auch sollte die Implementation einer Whitelist-Überprüfung gründlich geprüft werden, um mögliche Fehlkonfigurationen zu verhindern. Letztendlich ist es essenziell, dass Entwickler verstehen wie CORS genau funktioniert und welche Gefahren eine Fehlkonfiguration mit sich bringt, damit sichergestellt werden kann, dass die Webapplikationen sowohl sicher als auch funktional bleiben und keine unerwarteten Zugriffsbeschränkungen oder Sicherheitslücken entstehen.
Quellen / Ressourcen
[1] OWASP
https://owasp.org/
[2] OWASP TOP TEN
https://owasp.org/www-project-top-ten/
[3] MDN web docs
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
[4] PortSwigger
https://portswigger.net/web-security/cors
[5] YouTube
https://youtu.be/wgkj4ZgxI4c?si=3JRLABxOFfDfvswh
Informationen zum Autor: Annika Salzmann
Ein Programmierkurs hat bei mir als Kind die Leidenschaft für Computer und IT geweckt. Während meinem Informatikstudium habe ich dann meine Faszination für die IT-Security entdeckt. Seit Herbst 2022 darf ich nun bei der goSecurity, begleitend zu meinem Studium, meinem Wissensdrang nachkommen und meine Kenntnisse im Bereich Softwareentwicklung einbeziehen.











