CORS und die Same-Origin-Policy

15. September 2022
/Softwareentwicklung

In nahezu jedem Entwicklungsprojekt stößt man irgendwann unweigerlich auf Probleme mit Cross-Origin Requests, die sich meistens so äußern, dass Requests fehlschlagen und in der Entwicklerkonsole etwas steht wie XMLHttpRequest blocked by CORS policy.

Der Begriff “CORS” sagt somit fast jedem Entwickler etwas. Aber entgegen der weit verbreiteten Meinung, CORS sei ein Sicherheitsmechanismus im Web, bietet CORS uns stattdessen die Möglichkeit, Regeln zur Lockerung des eigentlichen Sicherheitsmechanismus zu definieren: der Same-Origin Policy. Um CORS zu verstehen, müssen wir also erst einmal über die Same-Origin Policy sprechen.

Die Same-Origin Policy

Auf developer.mozilla.org ist die Same-Origin Policy wie folgt beschrieben: The same-origin policy is a critical security mechanism that restricts how a document or script loaded by one origin can interact with a resource from another origin. Die Origin ergibt sich aus Protokoll, Host und Port der Website-Url. Hier ein paar Beispiele:

1. Website2. WebsiteSame-Origin?
https://example.com/foohttps://example.com/bar️✅ same origin
http://example.comhttps://example.com⛔️ unterschiedliche Protokolle
https://mail.example.comhttps://app.example.com⛔️ unterschiedliche Hosts
http://localhost:3000http://localhost:4000⛔️ unterschiedliche Ports

Vereinfacht gesagt bedeutet das also, dass eine Webseite von einer Origin nicht auf die Ressourcen einer Webseite mit einer anderen Origin zugreifen kann. Ressourcen sind zum Beispiel Bilder, Dokumente oder eine API – also alles, was über die URL der Website zu erreichen ist.

Aber wer verhindert letztendlich, dass wir auf die Ressourcen zugreifen? Das ist Aufgabe des Clients, also des Web-Browsers. Das ist sehr wichtig zu verstehen: der Server verhindert nicht den Zugriff auf seine Ressourcen und ist somit darauf angewiesen, dass der Client die Same-Origin Policy einhält. Nahezu alle modernen Web-Browser tun das, aber niemand hindert uns daran, außerhalb des Browsers einen cURL-Request an eine fremde Origin zu schicken und die Antwort zu lesen.

Ist die Same-Origin Policy damit nicht eigentlich sinnlos, wenn es so einfach ist, sie zu umgehen? Natürlich nicht, denn die Same-Origin Policy existiert nicht zum Schutz von Server-Ressourcen vor öffentlichem Zugriff. Was sie zu verhindern versucht, ist Cross-Site Request Forgery.

Was ist Cross-Site Request Forgery?

Wenn du dich in deinem Online-Banking-Account einloggst, dann schickt der Bankserver dir mit hoher Wahrscheinlichkeit eine Session-ID. Diese Session-ID kannst du nutzen, um dich bei diversen Aktionen in deinem Bank-Account – zum Beispiel einer Überweisung – zu authentifizieren.

Damit du dir die Session-ID nicht merken musst, speichert dein Browser sie im Hintergrund ab und schickt sie bei jedem Request an den Bankserver mit. Das geschieht ganz transparent, sodass du als Endnutzer nichts davon mitbekommt. Und genau hier liegt auch das Problem.

Während du in deinem Bank-Account eingeloggt bist, wird die Session-ID bei jedem Request an den Bankserver mitgeschickt, auch wenn der Request gar nicht von der Website deiner Bank kommt. Eine bösartige Webseite könnte das ausnutzen und sich dadurch unbemerkt Zugang zu deinem Bank-Account verschaffen. Diese Art von Angriff nennt man Cross-Site Request Forgery. Und genau das verhindert die Same-Origin Policy, denn dein Web-Browser lässt überhaupt nicht zu, dass die bösartige Website einen Request an deine Bank schickt.

Aber wie funktionieren dann...

…Micro-Services und Single-Page-Applications? Heutzutage ist fast jede Web-Applikation über mehrere Adressen im Netz verteilt. Zumindest das Frontend und Backend sind meist auf unterschiedlichen (Sub-)Domains zu erreichen. Wie kann das Frontend dann also mit dem Backend kommunizieren? Hier kommt endlich CORS ins Spiel.

CORS ist eine Spezifikation, die es dem Server erlaubt, Regeln zur Lockerung der Same-Origin Policy zu definieren. Zum Beispiel, welche fremden Origins auf Server-Ressourcen zugreifen dürfen. Dazu schickt der Server zu jedem Request eine Reihe von Response-Headern zurück, die den Cross-Origin Zugriff regeln. Ein Beispiel könnte so aussehen:

{
  "Access-Control-Allow-Origin": "https://your-webapp.de",
  "Access-Control-Allow-Credentials": true,
  "Access-Control-Expose-Headers": "Content-Type, Authorization"
}

Es gibt eine Reihe von CORS-Headern. Hier sind die wichtigsten im Überblick.

Access-Control-Allow-Origin

Wie der Name vermuten lässt, kontrolliert dieser Header, welche Origins Zugriff auf die Ressourcen des Servers haben. Es darf nur eine einzige Origin spezifiziert werden.

Access-Control-Allow-Origin: http://localhost:3000

Damit beliebige Origins Zugriff erhalten, muss der Server dynamisch auf jeden Request antworten, die Origin des Requests auslesen und als “allowed Origin” zurückschicken. Der Wildcard-Wert * erlaubt ebenfalls den Zugriff von beliebigen Origins, allerdings mit einer wichtigen Einschränkung: nur sofern der Request keine Credentials, das heißt Cookies oder Auth-Header, mitschickt.

Access-Control-Allow-Credentials

Damit überhaupt Cross-Origin Requests mit Credentials erlaubt sind, muss Access-Control-Allow-Credentials gesetzt sein.

Access-Control-Allow-Credentials: true

Access-Control-Allow-Headers

Eine Komma-separierte Liste an Request-Headern, die der Client schicken darf. Versucht der Client Header zu schicken, die hier nicht gelistet sind, schlägt der Request mit einem CORS-Fehler fehl.

Access-Control-Allow-Headers: X-Auth-Token, Your-Custom-Request-Header

Auch hier gibt es wieder die Wildcard *, und wieder gilt: Requests mit Credentials sind davon ausgeschlossen.

Access-Control-Expose-Headers

Eine Liste mit Response-Headern, die vom Client gelesen werden dürfen.

Access-Control-Expose-Headers: Auth-Token, Your-Custom-Response-Header

Standardmäßig darf der Client nur eine sehr begrenzte Liste an Standard-Response-Headern lesen. Im Network-Tab der Developer-Tools sieht man zwar alle Response-Header, wenn man versucht, über JavaScript darauf zuzugreifen, schlägt das allerdings fehl.

Natürlich gibt es wieder die Möglichkeit eine Wildcard über * zu definieren, von der Requests mit Credentials wieder ausgeschlossen sind.

Access-Control-Allow-Methods

Eine Komma-separierte Liste mit Request-Methoden, mit denen auf Ressourcen zugegriffen werden darf.

Access-Control-Allow-Methods: GET, POST, PUT

Der Sonderwert * erlaubt Zugriff über beliebige Request-Methoden — allerdings auch nur, wenn keine Credentials mitgeschickt werden. Access-Control-Allow-Methods wird von einigen Browsern, wie zum Beispiel Safari, nicht unterstützt.

Security through Obscurity

Erfüllt ein Request nicht die CORS-Anforderungen, führt das zwar zu einem Fehler beim Client, JavaScript liefert aber keine Details dazu. Diese sind ausschließlich über die Entwicklertools zugänglich. Das ist beabsichtigt und soll zusätzlich zur Sicherheit beitragen.

So funktionieren Cross-Origin Requests im Detail

Beim Zugriff auf Cross-Origin Ressourcen wird zwischen Simple-Requests und Preflighted-Requests unterschieden.

Simple-Requests müssen folgende Anforderungen erfüllen:

  • Die Request-Methode ist entweder GET, HEAD oder POST.
  • Die Request-Header enthalten ausschließlich Accept, Accept-Language, Content-Language, Content-Type oder Range.
  • Als Content-Type sind nur application/x-www-form-urlencoded, multipart/form-data und text/plain zulässig.
  • Und es dürfen keine Cookies oder Auth-Header mitgeschickt werden.

Requests, die diese Bedingungen nicht erfüllen, sind Preflighted. Das heißt, dass der Client vor dem eigentlichen Request einen sogenannten Preflight-Request schickt. Im Network-Tab der Entwicklertools kann man sehen, dass für so einen Request zwei Einträge geschrieben werden: ein OPTIONS-Request und der reguläre Request.

Das Network Tab der Chrome Developer-Konsole zeigt die Header eines erfolgreichen Preflight-Requests

Der Server antwortet darauf und schickt eine Liste mit CORS-Headern. Der eigentliche Request wird dann nur abgeschickt, wenn er die CORS-Bedingungen erfüllt.

Aber warum ist der Preflight-Request überhaupt nötig?

Wir erinnern uns, dass die Umsetzung der Same-Origin Policy und das Respektieren der CORS-Bedingungen die Verantwortung vom Client sind. Die Same-Origin Policy verhindert also nicht, dass ein Request gesendet wird (denn zu dem Zeitpunkt kennt der Client die CORS-Anforderungen des Servers noch nicht), sondern lediglich, dass die Response vom Client gelesen wird. Das bedeutet, dass Cross-Origin Requests erstmal immer abgeschickt und auf dem Server bearbeitet werden – inklusive eventueller Datenbank-Reads und -Writes. Ohne Preflight-Requests könnten wir zwar keine Server-Antworten lesen, wir könnten aber trotzdem schreiben und so gegebenenfalls den State des Servers verändern. Durch die Preflight-Requests wird das verhindert – auf Kosten eines zusätzlichen Requests, der für jede Anfrage notwendig ist.

Übrigens: auch wenn der Preflight-Request nicht die CORS-Anforderungen erfüllt, wird der eigentliche Request trotzdem in den Entwicklertools angezeigt. Allerdings unvollständig und ohne Response.

Das Network Tab der Chrome Developer-Konsole zeigt einen fehlgeschlagenen Preflight-Request ohne Response

Weitere Maßnahmen gegen CSRF

Die Same-Origin Policy ist nur eine Maßnahme, um Cross-Site Request Forgery zu verhindern. Idealerweise greifen mehrere Sicherheitsmechanismen ineinander und machen einen Angriff so fast unmöglich.

Um sich gegen CSRF zu schützen, werden zum Beispiel oft CSRF-Tokens genutzt. Es gibt außerdem die Möglichkeit, strikte Regeln festzulegen, wie ein Client die Cookies des Servers nutzen darf. Hier ein Beispiel:

Set-Cookie: _sid=KVok4uw5RftR38Wgp2BFwql; SameSite=Strict; Secure; HttpOnly

SameSite=Strict sorgt dafür, dass der Cookie nur bei Requests zur Same-Origin mitgeschickt wird. Mit Secure wird festgelegt, dass der Cookie nur über HTTPS versendet wird. Und mit HttpOnly verhindert man Zugriff auf den Cookie über JavaScript. Wie immer gilt: der Web-Browser ist dafür verantwortlich, diese Regeln einzuhalten.

Die SOP und CORS im Überblick

Wir haben gesehen, dass die Same-Origin Policy nötig ist, um uns vor CSRF zu schützen, und, dass CORS selbst kein Sicherheitsmechanismus ist, sondern uns die Möglichkeit gibt, die Same-Origin Policy zu lockern, wo es nötig ist.

Wir haben außerdem ein paar Besonderheiten über CORS gelernt. Zum Beispiel, dass das Wildcard-Pattern in CORS-Headern nur greift, wenn der Client keine Credentials sendet.

Und natürlich, dass CORS-Regeln zwar auf dem Server angepasst werden müssen, die Verantwortung für die Einhaltung der Regeln aber beim Client liegt. Apropos Verantwortung: die korrekte und sichere Implementierung der CORS-Regeln liegt natürlich in unserer Verantwortung als Entwickler. Nimm dir bei deinem nächsten Projekt doch ein paar Minuten Zeit, um darüber nachzudenken, wer eigentlich Zugriff auf deine Ressourcen bekommen sollte.

Johannes Stricker

Software Engineer