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:
-
Hat Airbnb eine eigene Übersetzungssoftware entwickelt?
-
Hat Airbnb von einer anderen Firma eine Übersetzungssoftware gekauft?
-
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:
- Der API-Anbieter, hier also
localhost:4013
. - Der Frontend-Server, hier also
127.0.0.1:8080
. - Der Nutzer, also Sie.
- 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