7. Persistenz, Autorisierung, Cookies
7.4 Cross-Site Request Forgery
Wenn wir eine interaktive Web-App schreiben, dann haben wir wohl folgenden Work-Flow vor Augen:
- Der Nutzer in die Adresszeile den Url der Startseite ein ein
- Der Server liefert die Startseite. Diese enthält mehrere Möglichkeiten,
mit dem Server zu interagieren - über
<a>
-Elemente, überform
-Elemente und über Ajax-Requests, die von Javascript veranlasst werden. - Die Interaktion des Nutzers mit der Startseite (oder weiteren Seiten) löst weitere Requests aus, die dann vom Server bearbeitet werden.
Das muss aber nicht immer so sein. Nichts hält mich davon ab, einen Link ins Innere einer Web-App hinein zu erstellen, beispielsweise Jakobs Speiselokal auf Facebook. Wenn Sie auf diesen Link klicken, dann werden Sie eben nicht zur Facebook-Startseite weitergeleitet, sondern direkt zu der Facebookseite des Restaurants. Und mehr noch: sollten Sie auf Facebook bereits in diesem Browser eingeloggt sein, dann erkennt das Facebook und weiß bereits, wer Sie sind.
Wann werden Cookies geschickt?
Sie haben bei dominikswebapp.de
ein Zugangskonto und sind gerade
eingeloggt. Der Server dominikswebapp.de
hat daher Cookies gesetzt,
z.B. session_id
, username
etc. Diese Cookies sind
im Browser gespeichert.
Ihr Browser schickt nun im Prinzip bei jedem Request an dominikswebapp.de
die Cookies mit.
Nicht aber an andere Seiten: wenn Sie eine Seite suspiciouswebsite.com
aufrufen, dann werden die Cookies, die zu dominikswebapp.de
gehören,
nicht mitgeschickt. Ansonsten könnte der Server von suspiciouswebsite.com
ja alle Session-Daten von dominikswebapp.de
mitlesen.
Stellen Sie sich nun vor, sie gehen - im gleichen Browser, in einem anderen Tab - auf
die Seite neutralwebsite.org
, und diese Seite enthält einen Link
auf dominikswebapp.de
hat, so wie ich oben mit
Jakobs Speiselokal auf Facebook.
Wenn Sie diesen Link klicken, dann erstellt Ihr Browser ja einen Request
auf dominikswebapp.de
. Schickt er die entsprechenden
Cookies mit? Ja. Die Richtlinie ist: geht ein Request vom Browser an
Host X, dann werden alle Cookies, die für Host X gesetzt wurden (ob vom Server X
selbst oder vom Browser) mitgeschickt.
Probieren wir es aber aus!
Demo Für diese Demo brauchen Sie im
Prinzip zwei "Hosts". Sie können es aber auch von einem Rechner aus tun, indem
Sie localhost
und 127.0.0.1
verwenden. Beide stellen
"Ihren" Rechner dar, werden aber von Ihrem Browser cookie-technisch als zwei
verschiedene Hosts wahrgenommen.
Übungsaufgabe Laden Sie sich 13-login-server.zip herunter oder zumindest die neueste Version von server.js. Dies ist der kleine Chat-Server mit Benutzerkonten aus dem vorherigen Teilkapitel. Gehen Sie in das Verzeichnis und starten Sie ihn:
und gehen im Browser auf http://localhost:4013.
Legen Sie ein Konto an (oder verwenden Sie einfach den Benutzernamen student
mit dem Passwort blabla
). Der Host http://localhost:4013
spielt
in dieser Demo die Rolle der vertrauenswürdigen Seite dominikswebapp.de
. Dort
haben
Sie ein Zugangskonto und darin haben Sie Daten gespeichert, an die andere nicht unbedingt
kommen sollten.
Gehen Sie jetzt
in das Verzeichnis 13-login-server/02-alien-posting/
und starten dort
einen Javascript-Httpserver:
Gehen Sie nun in einem weiteren Browser-Tab auf
127.0.0.1:8080/read-message-by-link.html.
Der Host http://127.0.0.1:8080
spielt nun der Rolle der verdächtigen (oder
zumindest
nicht vertrauenswürdigen Webseite suspiciouswebsite.com
.
Öffnen Sie die Javascript-Konsole und überzeugen Sie sich, dass kein Cookie gesetzt ist:
Schauen Sie auf dem anderen Tab, das mit http://localhost:4013
, nach Cookies:
Ihr Browser hat also für den Host localhost
Cookies gespeichert, nicht aber
für 127.0.0.1
. Gut! Nun gehen Sie wieder auf das Tab mit
127.0.0.1:8080/read-message-by-link.html
und klicken auf den Link
learn more
. Was geschieht? Der Html-Quelltext dieses Links ist
<a href="http://localhost:4013/main">zum Chat-Server</a>
.
Wenn Sie klicken, geht ein Request an localhost
. Offensichtlich werden
Cookies mitgeschickt, Sie werden nämlich gleich zur Chat-Seite weitergeleitet und nicht
zum Login-Bildschirm.
Demo
Jetzt wiederholen wir die vorherige Übung, schauen uns aber im Network-Tab der Konsole an,
was geschieht. Stellen Sie sicher, dass beide Server (an Port 4013 und an 8080) noch laufen,
und
Sie auf http://localhost:4013
eingeloggt sind.
Öffnen Sie wieder 127.0.0.1:8080/read-message-by-link.html
in einem Tab und öffnen die Konsole und dann den Reiter Network:
Laden Sie die Seite neu. Dann sehen Sie im Network-Tab nun die Requests, die das ausgelöst hat:
Ich habe die so sortiert (Reiter Waterfall), dass die ältesten unten stehen. Der
Request auf
read-message-by-link.html
wurde gesendet, als Sie die Seite neugeladen haben.
Da in dieser Html-Datei auf bootstrap.min.css
referenziert wird, wurde ein
zweiter Request geschickt. Wenn Sie nun auf den Request
read-message-by-link.html
klicken,
können Sie sich die Http-Header im Quelltext ansehen:
Klicken Sie nun auf den Link "zum Chat-Server" und schauen sich den Header
von main
an:
Schauen wir uns die rot markierten Header an:
Cookie: username=dominik; session_id=0.7691479075795467 Host: localhost:4013 Referer: http://127.0.0.1:8080/
Wenn nun der Klick auf den Link einen Request auf einen fremden Host auslöst
(der jetzige Host im Browser-Tab ist 127.0.0.1
; der Host localhost
ist
also fremd), dann schickt der Browser das Cookie mit, teilt dem Server aber
mit Referer: http://127.0.0.1:8080/
mit, dass der Request "von draußen" kam.
Übungsaufgabe
Erstellen Sie eine Kopie von
read-message-by-link.html,
bei der der Klick Sie aus dem Login-Server auf localhost:4013
ausloggt.
Was Sie in der letzten Übung getan haben, nennt sich Cross-Site Request Forgery. Eine Seite generiert einen Request an eine andere (oder verführt den Nutzer, so einen zu generieren), daher Cross-Site. Da der Nutzer diesen Request nicht beabsichtigt, und er auch von einer Quelle stammt, die von der App nicht vorgesehene ist, reden wir von Fälschung, also Forgery. Immerhin: der Nutzer bekommt es mit, weil er ja auf die neue Seite geleitet wird.
Gefahrenquellen
Wir haben oben also gesehen: wenn eine "vertrauenswürdige" Siete wie
localhost:4013
(der Login-Server) Cookies gesetzt hat und eine
weitere, nicht vertrauenswürdige Seite auf localhost:4013
verlinkt,
dann schickt der Browser einfach mal alle Cookies mit, falls der Nutzer diesen Link
anklickt.
Gefahrenquelle 1. Eine Gefahrenquelle haben wir bereits gesehen. Ein
unbedarfter Klick auf einer "dritten" Webseite (127.0.0.1:8080
) hat Sie aus der
"ersten"
(der Login-Server auf localhost:4013
) ausgeloggt.
Schlimmer noch wäre es, wenn ein Klick auf so einen Link einen Beitrag
auf dem Chat-Server erzeugen würde:
Ein Angreifer könnte zwar nicht an Ihre persönlichen Daten ran, kann Sie (bzw. Ihren Browser) aber zu einer Transaktion veranlassen, die Sie so nicht gewollt haben.
Übungsaufgabe Schreiben Sie eine Webseite, die eben dies tut: der Klick auf einen Link erzeugt einen neuen Beitrag im Chat-Server, falls der Nutzer dort eingeloggt ist.
Hinweis: dafür müssen Sie nicht nur die nicht vertrauenswürdige Webseite read-message-by-link.html umschreiben, sondern den Quelltext des Login-Servers in ändern.
Übungsaufgabe Erstellen Sie eine weitere Version der Seite aus der letzten Aufgabe. Bei dieser bekommt der Nutzer ja wenigstens mit, dass er betrogen wurde und unter seinem Namen ein Beitrag geschrieben wurde, weil der Browser ihn ja zum Chat-Server weiterleitet.
Schreiben Sie eine Seite, die ein <img>-Element enthält. Das
src
-Attribut soll aber keine Bild-Datei sein, sondern
eben jener bösartige Link, der einen kompromittierenden Beitrag
auf dem Chat-Server erzeugt. Funktioniert das?
Ein GET
sollte nie eine "semantische" Änderung auf dem Server
verursachen.
/logout
, /addmessage
, /create-account
,
/transfer-money
, /submit-exmatrikulation
sollten alles
POST
-Requests sein.
Warum? Weil ein Klick auf einen Link (also ein <a>
-Tag)
immer einen GET
-Request auslöst.
Ich habe oben von semantischer Veränderung gesprochen, denn natürlich gibt es
legitime Gründe, wo ein GET
-Requests den Zustand des Servers ändern kann,
zum Beispiel weil der Server einfach Buch führen will und jeder GET
-Request
einen Zähler inkrementiert.
Übungsaufgabe
Ändern Sie Ihre lokale Kopie von server.js
und ändern /logout
in einen POST
-Request um. Dann müssen Sie natürlich auch den
Logout-Knopf ändern.
Gefahrenquelle 2. Nehmen wir nun an, unsere App
befolgt die Richtlinie, dass GET
-Request nur lesen und
nie die Daten ändern (außer für den Nutzer irrelevante wie server-seitige Statistiken).
Dennoch kann es sein, dass Sie schlicht und ergreifend nicht wollen,
dass Sie ein Klick auf einen Link ungefragt zum Chat-Server schickt und Sie dann
gleich eingeloggt sind.
Dass ein Klick auf einen Link Sie zu einer anderen Seite weiterleitet, wollen
Sie ja nicht grundsätzlich verhindern. Das ist ja gerade der Sinn des Webs.
Aber Sie wollen vielleicht nicht, dass Sie gleich als "eingeloggt" behandelt werden,
weil eventuell andere Leute (Arbeitskollegen etc.) Ihren Bildschirm sehen.
Sie als Nutzer können da nicht viel tun (außer sich die Adresse des Links anzuschauen,
bevor Sie draufklicken).
Wer das verhindern kann, ist der Server. Wenn der Server nicht will,
dass ein einfacher Klick Sie direkt von einer dritten Webseite auf die
Chat-Seite bringt, kann er dies verhindern: der Server kann dem Cookie
den Text SameSite=Strict
hinzufügen und damit dem Browser
signalisieren, dass Cookies nicht mitgeschickt werden sollen, wenn
der Nutzer durch einen Klick auf einen Link auf die Seite gekommen ist.
Die Same-Site-Policy
Wann schickt ein Klick auf einen Link die Cookies mit? Werden Cookies
mitgeschickt, wenn die Seite automatisch Resourcen lädt (wie zum Beispiel Bilder)?
Das hängt von den SameSite-Einstellungen ab, die der Server den Cookies
mitgegeben hat. Bei der Recherche zu der Frage, ob beim Laden
einer zusätzlichen Resource, beispielsweise einer
mit <img>
eingebundenen Bilddatei, Cookies mitgeschickt werden,
bin ich auf viele Quellen gestoßen, die behaupten Ja, die werden mitgeschickt.
Allerdings sind viele dieser Quellen von 2009 und mittlerweile veraltet.
Hier ein Post dazu:
This question is old, but was the first result on Google for me, so I think it's worth clarifying how this works nowadays (2021).
When bar.com sets the cookie, they can specify a
SameSite
attribute.If the cookie is set with SameSite=Lax (or the SameSite attribute is not specified), then the cookie will not be sent for requests for images/iframes/etc hosted on bar.com, but will be sent if the user clicks a link on your foo.com homepage that takes them to bar.com
If the cookie is set with SameSite=Strict, the cookie will not be included in requests to bar.com that originate from another webiste, including if the user clicks a bar.com link on foo.com.
If the cookie is set with SameSite=None, the cookie will be sent to bar.com, including requests for images.
Wir können für SameSite
drei verschiedene Werte anlegen:
Strict
, Lax
und None
. Die Bedeutung
ist auf
developer.mozilla.org
wie folgt spezifiziert:
Means that the browser sends the cookie only for same-site requests, that is, requests originating from the same site that set the cookie. If a request originates from a different domain or scheme (even with the same domain), no cookies with the SameSite=Strict attribute are sent.
Strict
Means that the cookie is not sent on cross-site requests, such as on requests to load images or frames, but is sent when a user is navigating to the origin site from an external site (for example, when following a link). This is the default behavior if the SameSite attribute is not specified.
Lax
means that the browser sends the cookie with both cross-site and same-site requests. The Secure attribute must also be set when setting this value, like so SameSite=None; Secure. If Secure is missing an error will be logged [...]
None
Hier nochmal auf Deutsch in meinen eigenen Worten:
-
SameSite=Strict
: der Browser schickt die Cookies nur dann mit, wenn die Seite, die den Request erzeugt, über den gleichen Host geladen wurde, der auch die Cookies gesetzt hat. "Gleicher Host" ignoriert hier den Port,localhost:4013
undlocalhost:8080
wären hier also gleiche Hosts; daher haben wir127.0.0.1:8080
verwendet, damit es zwei verschiedene werden). Die Cookies werden natürlich auch mitgeschickt, wenn der User den URL per Hand (oder Copy-Paste) oben in die Adresszeile eingibt und Enter drückt. -
SameSite=Lax
: wie Strict, es werden allerdings Cookies geschickt, wenn der Nutzer durch Klick auf einen Link zur Cookie-setzenden Seite gelangt. Dies ist die Grundeinstellung (gilt also, wenn das Attribut nicht gesetzt ist). -
SameSite=None
: dann werden die Cookies bei jedem Request geschickt, egal woher, egal durch der Request ausgelöst worden ist. Allerdings kann der Server das nur setzen, wenn auch das Attributsecure
gesetzt ist. Das wiederum heißt, dass das Cookie nur geschickt werden darf, wenn die Kommunikation über Https läuft. Um damit zu experimentieren, müssen wir also den Server auf Https umrüsten.
Übungsaufgabe
Machen Sie den Chat-Server unsicherer, indem Sie
SameSite=None
setzen. Hierfür müssen Sie ihn
auf Https umrüsten.
Hosten Sie nun auf 127.0.0.1:8080
eine Seite,
in der ein "Bild" eingebunden ist, dessen src
aber
nicht auf eine Bilddatei zeigt, sondern Sie vom Chat-Server ausloggt.
Übungsaufgabe Machen Sie den Chat-Server sicherer/restriktiver, so dass der User nicht mehr über Links von Drittseiten auf das Chat-Fenster gelangen kann (sondern wie ein ausgeloggter Nutzer behandelt wird).
Wenn ich beim Login-Server SameSite=Strict
gesetzt habe und nun
in read-message-by-link
den Link klicke, dann schickt der Browser die Cookies nicht mit. Der Server
lädt also nicht /main
sondern schickt mich zum Login-Bildschirm
zurück, mitsamt Fehlermeldung, dass ich nicht eingeloggt bin.
Allerdings interessant, was jetzt passiert, wenn ich auf dieser Seite bin:
- Wenn ich auf das Reload-Symbol drücke, dann werde ich immer noch wie
nicht eingeloggt behandelt. Schauen Sie sich den Request im Http-Quelltext an.
Da steht immer noch
Referer: http://127.0.0.1:8080
drin, egal, wie oft ich auf Reload drücke. Der Browser behandelt es also immer noch als Cross-Site-Request. - Wenn ich in die Adresszeile klicke und Enter drücke, dann bin ich plötzlich eingeloggt. Denn nun habe ich als Nutzer ja oben in der Zeile selbst die Seite angesteuert. Dies zählt nun also als Same-Site-Request.
-
Wenn ich auf den Link back to main page
drücke, dann zählt das auch als Same-Site-Request, weil ja die erzeugende
Seite von
localhost:4013
gehostet wird.
Übungsaufgabe Überprüfen Sie, ob Ihr Browser das gleiche Verhalten zeigt.
Gefahrenquelle 3.
Es gibt eine weitere Sicherheitslücke. Eine Webseite kann automatisch
weiterleiten auf eine andere. Wenn es zum Beispiel auf Ihrem
Server eine Datei /public/main-old.html
liegt, dann wird
die ja vom Server "statisch" bedient, denn server.js
enthält
die Codezeile
app.use(express.static(path.join(__dirname, "..", "public")));
Es wird also nicht mit app.get("/public/main-old.html")
oder so
eine Funktion definiert, die Cookies überprüfen könnte. Nein,
die Seite wird statisch geladen (so wie beispielsweise auch
public/styles.css
). Das Problem ist jetzt allerdings,
wenn aus irgendeinem Grund die Seite
/public/main-old.html
einen automatischen Weiterleitungsbefehl
auf /main
gibt. Denn
/public/main-old.html
kann ohne Cookies geladen werden;
leitet dann weiter auf eine Seite im "Cookie-Bereich"; da
/public/main-old.html
aber bereits vom Host localhost:4013
geladen wird, zählt die Weiterleitung als Same-Site-Request, und der
Browser schickt die Cookies mit. Und sofort werden die Nachrichten angezeigt:
Übungsaufgabe
Legen Sie im Verzeichnisbaum vom Chat-Server eine Datei /public/main-old.html
an, die den Nutzer auf /main
weiterleitet. Dann
erstellen Sie im Verzeichnis, wo 127.0.0.1:8080
läuft, eine Seite
suspicious-link-to-main-old.html
, die einen Link auf
/public/main-old.html
enthält.
Gehen Sie auf 127.0.0.1:8080/suspicious-link-to-main-old.html
und klicken Sie auf den Link. Funktioniert die automatische Weiterleitung?
Werden Cookies mitgeschickt?
SameSite=Strict
haben wollen, dann
stellen Sie bitte sicher, dass Ihre "ungeschützten" Seiten (für die
der Server nicht die Cookies überprüft) nicht irgendwie auf die
geschützten Seiten weiterleiten; denn sonst könnte man
von außen doch den Browser dazu bringen, die Cookies mitzusenden.
Wir haben bis jetzt drei Gefahrenquellen identifiziert. Als erstes die,
dass der Nutzer durch unbedarftes Link-Klicken einen
GET
-Request veranlasst und dieser Server Nutzerdaten ändert;
dies können wir verhindern, indem wir serverseitig
alle verändernden Requests als POST
definieren.
Zweitens, dass ein Link-Klick über einen GET
-Request womöglich private Inhalte
auf den Bildschirm lädt und somit physisch anwesende dritte Personen Dinge sehen, die sie
nicht sehen sollen. Das Szenario verhindern Sie, indem Sie serverseitig
SameSite=Strict
einstellen.
Drittens, dass intern auf den Seiten im Server irgendwo eine automatische
Weiterleitung eingerichtet ist, von einer nicht-Cookie-geschützten Seite auf eine
Cookie-geschützte, und ein Cross-Site-Request somit einen
Same-Site-Request auslöst, der dann alle Cookies mitschickt.
Alle Gefahrenquellen drehen sich also um Cross-Site-Requests und die daran
haftenden Cookies.
Neben dem Klicken auf Links, dem Reload-Knopf, der Adresszeile,
<img>
-Elementen und Weiterleitungsbefehlen gibt es ja
noch weitere Mechanismen, die einen Request auslösen können. Schauen wir uns
die doch mal an.
Requests, die durch eine form
ausgelöst werden
Übungsaufgabe
Legen Sie im Verzeichnis, wo 127.0.0.1:8080
läuft, eine
Datei an, wo über eine form
ein POST
an
http://localhost:4013/add-message
geschickt wird.
Sie können den kompromitierenden Inhalt sogar verbergen,
indem Sie dem input
-Element die Attribute
style="display:none"
und
value="Ihre gewünschte Fake-Nachricht
geben.
Sie werden bei der letzten Übung scheitern. Die form
schickt
die Cookies nicht mit. Warum? Es ist wohl so wie bei
img
, dass Cookies nicht mitgeschickt werden, wenn
sameSite=Lax
oder sameSite=Strict
gesetzt ist.
Übungsaufgabe
Geben Sie dem Chat-Server eine Sicherheitslücke, indem Sie ihn
mit sameSite=None
starten. Denken Sie an Https!
Können Sie von der fremden Seite auf 127.0.0.1:8080
per
Knopfdruck Nachrichten Ihrer Wahl auf
localhost:4013
veröffentlichen?