7. Persistenz, Autorisierung, Cookies

7.6 Web-APIs und CORS

Erinnern Sie sich an den Login-Server 13-login-server.zip und das Übersichtsdiagramm:

Jedes violett Polygon ist ein Endpoint des Login-Servers, sprich eine Methode, die aufgerufen werden kann. Viele davon entsprechen mehr oder weniger einer Html-Seite (manchmal dynamisch erzeugt): GET / lädt die Datei home.ejs (bzw. die Html-Datei, die mithilfe der render-Methode des Packetst ejs daraus erstellt wird), GET /login lädt die Datei login.ejs und so weiter. Nicht in das Schema passt GET /get-messages.

Demo Starten Sie den Login-Server:

node src/server.js

gehen dann auf localhost:4013 und loggen sich ein. Gehen Sie dann auf localhost:4013/get-messages. Da wird keine Html-Seite geladen, sondern "rohe" Daten im JSON-Format. Die Idee ist: Die Seite /main enthält also Javascript-Code, der per Ajax einen Html-Request auf /get-messages schickt und das Ergebnis als Tabelle darstellt.

Der Vorteil: es müssen nur die Rohdaten übertragen werden. Das Erstellen der Tabelle und des ganzen Html drumherum geschickt innerhalb des Browsers. Für den Server ist der Vorteil, dass er weniger arbeiten muss; für den User, dass man nicht das Gefühl hat, die Seite würde neu geladen. Sie bleibt voll funktionsfähig, während der Http-Request durch das Internet geschickt wird.

Das war bis jetzt bei unseren Anwendung immer so: der Server stellt Endpoints zu Verfügung, die entweder direkt vom User oder von Html-Forms und Links geladen werden, oder eben von Ajax-Requests. Diese Html-Forms und Ajax-Requests befanden sich auch immer auf Seiten, die selbst vom gleichen Server erstellt wurden. Es gab also nur zwei Parteien: Nutzer (plus Browser) und Server. Wenn eine dritte Partei hinzukam, die "von außen" Requests schickt, wie im Kapitel 7.4, dann haben wir das meist als unlauteren Zugriff betrachtet und in der Tat verschiedene Angriffsszenarien durchgespielt. In diesem Kapitel jedoch geht um völlig legitime Anwendungsfälle, wo die Endpoints von einer dritte Partei verwendet werden.

Ein weiterer Endpoint des Login-Servers

Unser Login-Server hat allerdings noch einen weiteren Endpoint, der nicht von irgendeiner eigenen Seite verwendet wird: GET /fib-raw. Probieren Sie es: öffnen Sie http://localhost:4013/fib-raw?n=20 in einem neuen Tab. Das Ergebnis ist keine Html-Seite, sondern reiner Text: {"result":6765}. Der Server auf localhost:4013 stellt uns also nicht nur Webseiten (in Html) zur Verfügung, sondern auch die Dienstleistung, über den Url localhost:4013/fib-raw mit Suchparametern im Format n=... Fibonacci-Zahlen zu berechnen. Dieser Service kann nun von jeder Applikation benutzt werden, die Http-Requests erstellen kann. In Fachsprache: der Server localhost:4013 stellt ein API (Application Programmer Interface) zur Verfügung, mit dessen Hilfe man Fibonacci-Zahlen berechnen kann. Bringen Sie bitte die mentale Flexibilität auf, die doch in ihrer Nützlichkeit beschränkte Dienstleistung Fibonacci-Zahlen berechnen durch etwas Sinnvolleres zu ersetzen, beispielsweise Text von Polnisch auf Deutsch übersetzen.

Anwendungsszenario Die Fluggesellschaft FG unterhält ihre eigene Webseite, über die Kunden Flüge suchen und buchen können. Diese tätigen Ajax-Requests an Endpoints des Servers, z.B. GET /search?from=BER&to=SHA&date=2024-02-30.

Nun könnte eine dritte Webseite auch Ajax-Requests an den FG-Server schicken. Will FG das? Unter Umständen ja: die dritte Partei könnte ja ein großes Reisebüro RB sein, dessen Webseiten Anfragen auf FG schicken wollen. Es ist also im Interesse von FG, seine Endpoints auch Dritten zur Verfügung zu stellen. Das impliziert auch, dass GET /search idealerweise keine fertige Html-Seite liefert sondern Rohdaten in einem wohlspezifizierten JSON-Format. Denn nur so können die Entwickler von RB es verwenden. Und FG hat ja kommerzielles Interesse daran, dass Kunden von RB auch FG-Flüge finden.

Übungsaufgabe Was, wenn FG explizit nicht will, dass Endpoints wie GET /search von Drittanbietern wie RB angefragt werden? Wie kann FG das verhindern?

Übungsaufgabe Was, wenn eine Firma XY Endpoints wie GET /private-address?firstname=Dominik&lastname=Scheder nur zahlenden Kunden zur Verfügung stellen will?

Anwendungsszenario Auf Seiten wie beispielsweise AirBnb werden Kundenbewertungen automatisch übersetzt, so dass plötzlich auch Gäste aus Korea Bewertung auf Deutsch zu hinterlassen scheinen. Wie kriegt Airbnb das hin? Wie würden Sie vorgehen, wenn Sie so eine automatische Übersetzung in Ihrer Applikation anbieten wollen:

  1. Hat Airbnb eine eigene Übersetzungssoftware entwickelt?

  2. Hat Airbnb von einer anderen Firma eine Übersetzungssoftware gekauft?

  3. Nutzt Airbnb eine Web-API, indem Sie einem Übersetzungsanbieter Http-Requests wie

    GET /translate?target=DE&source=EN&content=the%20service%20was%20great%20but%20the%20food%20sucked HTTP/1.1                                      
    host: www.translate.com
    ...

    schickt und den Response in die eigene Webseite integriert? Und dafür bezahlt?

Ohne die Antwort zu kennen, würde ich Option 3 für die plausibelste halten. Option 3 hat gegenüber Option 2 für den Kunden (hier: Airbnb) den Vorteil, dass er nie Updates installieren muss. Für den Anbieter (hier: Google), dass eine Software im Extremfall reverse-engineert werden könnte oder der Kunden zumindest wertvolle Daten in Hände bekommen würde. Und dass man unter Option 3 präzise nach Verbrauch bepreisen kann (1 Cent pro 100 Wörter...).

Jetzt sind Sie dran! Erinnern Sie sich daran, wie man mit Javascript Http-Requests erstellt (Ajax, Kapitel 6.1).

Übungsaufgabe Schreiben Sie ein Frontend, auf welchem der User eine Zahl \(n\) eingeben kann, für welche dann die Fibonacci-Zahl \(F_n\) berechnet und ausgegeben wird. Führen Sie die Berechnung nicht lokal im Browser aus, sondern verwenden Sie die API vom Login-Server auf localhost:4013, also 13-login-server.zip.

Im besten Falle scheitern Sie hierbei. Schauen Sie sich in der Javascript-Konsole die Fehlermeldung an.

Cross-Site Resouce Sharing

Wenn Sie die vorherige Übungsaufgabe erfolgreich gemeistert haben und ein Frontend, nennen wir es fib-frontend.html, ersetllt haben, dann sehen Sie, dass der Ajax-Request Ihrer Seite gescheitert ist und folgende Fehlermeldung erzeugt hat:

Access to XMLHttpRequest at 'http://localhost:4013/fib-raw?n=8' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Deutlicher wird es noch, wenn Sie Ihr Frontend fib-frontend.html nicht direkt vom File-Explorer öffnen sondern es separat von einem Http-Server aus hosten:

cd fibonacci-frontend
http-server .

und dan auf 127.0.0.1:8080/fib-frontend.html klicken. Dann schaut die Fehlermeldung so aus:

Access to XMLHttpRequest at 'http://localhost:4013/fib-raw?n=8' from origin 'http://127.0.0.1:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Was passiert? Das Problem ist, dass localhost:4013 und 127.0.0.1:8080 zwei verschiedene Server sind. Die Seite fib-fontend.html wurde über letzteren geladen, der Service /fib-raw aber bei ersterem angefragt. Nun ist es bei Ajax-Requests üblicherweise so, dass der Request an den gleichen Server geschickt wird, von dem die Seite geladen wurde. Wenn nun diese Server nicht identisch sind, dann ist "etwas im Gange", und die Grundeinstellung ist, den Request zu blockieren. Das erinnert uns an die Same-Site-Policy, die geregelt hat, wann Cookies mitgeschickt werden sollen, wenn ein Request an einen anderen Server geht.

Terminologie Ein Ajax-Request zu einem anderen Server als dem, von welchem die Seite geladen wurde, nennt man einen CORS-Request (cross origin resource sharing).

Wir haben gerade gesehen, dass in der "Grundeinstellung" alle CORS-Requests blockiert werden.

Um zu verstehen, wie und warum CORS-Requests blockiert werden und wie man diese Blockierung aufhebt, müssen wir verstehen, welche Parteien hier involviert sind:

  1. Der API-Anbieter, hier also localhost:4013.
  2. Der Frontend-Server, hier also 127.0.0.1:8080.
  3. Der Nutzer, also Sie.
  4. Der Browser, in meinem Falle Google Chrome.

Übungsaufgabe Überlegen Sie, was mit dem Blockieren des Cross-Site-Requests, das wir oben erlebt haben, erreicht werden soll. Wer schützt hier wer vom wem geschützt werden? Schützt der Server sich vor unauthorisierten Anfragen? Schützt der Browser den Server? Der Server den Nutzer?

Übungsaufgabe Finden Sie heraus, was zwischen Browser und Server geschieht. Bei mir reicht dafür nicht aus, die Developer-Tools des Browsers zu öffen und den Reiter "Network" zu wählen. Ich musste mit ServerSniffer.java aktiv belauschen, welche HTTP-Nachrichten zwischen Browser und Server (hier: localhost:4013) ausgetauscht werden.

Belauschen Sie mit den Sniffer die Kommunikation! Achten Sie insbesondere auf den Response des Servers! Hosten Sie hierfür Ihr fib-frontend.html per http-server auf 127.0.0.1:8080, statt es direkt als Datei im Browser zu öffnen!

Tip: Sie können den Login-Server auf einem anderen Port starten mit node src/server.js --port 4014 zum Beispiel.

Wenn Sie die letzte Übungsaufgabe gelöst haben, dann sehen Sie, dass der API-Server durchaus den Request beantwotet hat und die Fibonacci-Zahl \(F_n\) schickt. Es ist also der Browser, der den Response zwar erhält, sich aber weigert, die Daten dem Javascript-Code zur Verfügung zu stellen! Es ist also der Browser, der den Request blockiert und jemanden schützen will. Oder besser gesagt: der Browser blockert nicht den Request, sondern den Response!

Übungsaufgabe Wiederholen Sie die vorherige Übung, ändern aber fib-frontend.html und server.js, sodass nun ein Endpoint /fib-raw-post verwendet wird, der als Methode POST nutzt statt GET. Schickt der Browser nun auch diesen Request? Ändert sich etwas im Vergleich zu GET?

Diskussion Überlegen Sie, wen der Browser vor wem schützen will mit diesem Verhalten und warum.

  • Den Frontend-Server zu schützen scheint mir keinen Sinn zu machen. Denn dieser ist es ja, dessen Javascript-Code den Request überhaupt erst initiiert. Wenn so ein Request gefährlich für den Frontend-Server wäre, dann dürfte der halt keine solchen Ajax-Requests in seine Webseiten einbauen.
  • Den API-Server schützen? Ergibt auch keinen Sinn. Denn der Server beantwortet ja bereitwillig den Request. Und überhaupt kann im Ernstfall ein Browser einen Server nicht schützen, da Angreifer ja ihre eigenen, korrumptierten Browser (oder einfach User-Agents) verwenden können.
  • Soll der User geschützt werden vor dem Frontend-Server? Wie bei der Same-Site-Policy will der User vielleicht nicht, dass eine solche Anfrage an den API-Server stattfindet. Denn vielleicht ist es ja kein API-Server, sondern ein Banken-Server, wo der User gerade eingeloggt ist. In diesem Falle bestünde ein Sicherheitsrisiko aber nur, wenn beim Ajax-Request Cookies mitgeschickt würden (was sie nicht werden im Normalfall).

Solange keine Cookies mit im Spiel sind, ist mir ehrlich gesagt nicht ganz klar, was mit dem Blockieren erreicht werden soll.

Übungsaufgabe Beschreiebn Sie mir ein "Angriffsszenario", also ein Szenario, in welchem der Nutzer gefährdet wäre, wenn sein Browser den CORS-Request nicht blockieren würde.

CORS-Requests erlauben

Ein API kann ja nur dann seine Dienste anbieten, wenn es CORS-Requests erlaubt. Aber wir haben ja gesehen: der API-Server beantwortet die CORS-Requests ja in jedem Fall. Es ist der Browser, der sich eventuell weigert, das Ergebnis weiterzuverwenden. Hierfür muss nun also der API-Server dem Browser signalisieren, dass er damit einverstanden ist, dass der Response des CORS-Requests verwendet wird. Hierfür wird im Response der Header Access-Control-Allow-Origin gesetzt, gefolgt von einer Liste von Dritt-Servern, für die CORS-Requests erlaubt sein sollen. In Node.js sieht das so aus (Zeilen 61-65 im Quelltext von 13-login-server/src/server.js):

  const cors_options = {
    origin: ['http://localhost:8080', 'http://127.0.0.1:8080'],
    credentials: cors_credentials
  }
  app.use(cors(cors_options));

Übungsaufgabe Starten Sie meinen Login-Server mit der Option --cors:

node src/server.js --cors

Mit dieser Option aktiviere ich Zeilen 61-65. Öffnen Sie nun Ihr fib-frontend.html, und zwar einmal auf 127.0.0.1:8080 gehostet, einmal direkt als Datei geöffnet und schauen, was funktioniert und was nicht.

Übungsaufgabe Ändern Sie nun Zeile 62 im Code des Servers:

  origin: '*', // origin: ['http://localhost:8080', 'http://127.0.0.1:8080'],

Hiermit erlauben Sie CORS-Requests von allen Adressen. Funktioniert fib-frontend.html nun auch, wenn Sie es direkt als Datei geöffnet haben?

Übungsaufgabe Was geschieht, wenn Sie im Server die CORS-Policy auf '*' gesetzt haben, das fib-frontend.html aber nun über Ihre "echte" IP-Adresse ansteuern, also beispielsweise http://141.46.220.23:8080/fib-frontend.html