Luca: Warum SameSite Cookies nicht die eierlegenden Wollmilchsäue in Sachen CSRF-Protection sind

In diesem Beitrag geht’s um die Luca Locations Web-App und um einen unzureichenden Schutz vor „Website-übergreifende Anfragenfälschung“ (CSRF). Die hier beschriebenen Lücken wurde bereits geschlossen.

Den Begriff „Cross-Site-Request-Forgery“ sollten die meisten Webentwickler:innen kennen. Konkret ist damit das „Fälschen“ von HTTP-Anfragen von einer fremden Website gemeint (siehe Wikipedia). Um die eigene Website bzw. dessen API davor zu schützen, verwenden viele Betreiber sogenannte Anti-CSRF-Tokens. Diese Tokens werden bei jedem Request übermittelt und im Backend validiert. Da bestenfalls nur die Website selbst den korrekten CSRF-Token kennt, kann so die Integrität der Anfragen gewährleistet werden. Diese „klassische“ Schutzmaßnahme ist aber bei Weitem nicht die einzige Möglichkeit, sich vor derartigen Angriffen zu schützen. In den letzten Jahren sind einige neue Browser-Spezifikationen hinzugekommen, welche sich mehr oder weniger gut dazu verwenden lassen. Unter anderem die Abwärtskompatibilität kann bei der Verwendung älterer Browser-Version ein Problem sein.

Luca Locations

Die Luca Locations Web-App ist zur Verwaltung der Daten wie bspw. QR-Codes für Location-Betreiber gedacht. Anders als bei der App für’s Check-In ist hier ein Benutzerkonto erforderlich, welches auf dem Server gespeichert wird. Es gibt somit eine Login-Funktion und eine Session sowie ein Session-Cookie.

Um die Fälschung von Anfragen im Kontext des eingeloggten Benutzers zu verhindern, wird anstelle eines CSRF-Tokens das Session-Cookie mit dem SameSite-Flag markiert. SameSite Cookies werden im Request nicht übermittelt, wenn der Initiator nicht dem Besitzer des Cookies entspricht. Hierbei gibt es zwei Stufen: Lax und Strict. Luca hat sich für Letzteres entschieden, wodurch sämtliche Anfragen aus fremden Quellen ohne das Session-Cookie übermittelt werden.

Sofern alle Browser-Hersteller diese Spezifikation richtig implementiert haben, ist Session-Riding nicht möglich. Bis auf den Internet Explorer ist dieser Standard auch überall vorhanden. (Siehe Browser compatibility)

Während der Session ist vor der Session

Wenn man in Luca Locations eingeloggt ist, sieht man die angelegten Locations, sofern man bereits welche erstellt hat. Was man jedoch nicht direkt sieht, ist der Benutzername oder die E-Mail-Adresse des Accounts. Es ist somit nicht ersichtlich, in welchem Account man sich befindet.

Aus diesem Zustand und einer meiner Meinung nach etwas ungünstigen Implementation des SameSite-Standards seitens der Browser-Hersteller ergab sich folgendes Problem:

Das Session-Cookie konnte durch einen weiteren Login-Request mit fremden Zugangsdaten überschrieben werden, wodurch man sich unbemerkt in einem fremden Account befand.

Denn der API-Endpunkt zum Login benötigt logischerweise kein gültiges Session-Cookie und konnte daher auch von einer fremden Website angesprochen werden. Nach einem erfolgreichen Login, antwortete der Server mit einem weiteren Session-Cookie, welches das bereits vorhandene überschrieb.

Voraussetzung zur erfolgreichen Durchführung eines Angriffs war, dass der API-Endpunkt auch application/x-www-form-urlencoded anstelle application/json als Content-Type akzeptiert. Denn das Absenden der POST-Anfrage ist nur per HTML-Form möglich, da ansonsten Set-Cookie-Anweisungen verworfen werden. Tatsächlich akzeptierte das Node.JS-Backend auch andere Content-Types. Weitere Schutzmaßnahmen gab es zu dem Zeitpunkt nicht.

Ein Proof of Concept

Zunächst habe ich ein HTML-Dokument erstellt, welches den Login-Request per POST-Anfrage an die Luca-API sendet:

    <form action="https://app.luca-app.de/api/v3/auth/login" method="post" name="loginForm"> 
        <input type="hidden" name="email" value="angreiferaccount@byom.de"> 
        <input type="hidden" name="password" value="MoinLuca"> 
    </form> 

    <script> 
        loginForm.submit(); 
    </script>

Beim Absenden der Form, erhält der Server folgende Anfrage

Wie man erkennen kann, wurden die Login-Informationen nicht als JSON, sondern Form-encoded übermittelt. Auch die Tatsache, dass die Anfrage von einer fremden Origin/Quelle stammt, störte den Server nicht.

Die Anfrage wurde mit 200 OK beantwortet und das bereits bestehende Session-Cookie wurde durch ein neues ersetzt, wodurch sich der Nutzer von nun an in einem fremden Account befand. Obwohl das neue Session-Cookie ebenfalls als SameSite=Strict markiert wird, hindert es den Browser nicht daran dieses zu setzen.

Was ermöglichte diese Schwachstelle?

Ein Angreifer konnte einen Location-Betreiber auf eine präparierte Website leiten. Diese Website konnte unbemerkt (im Hintergrund) ein Login in einem vom Angreifer kontrollierten Luca-Location-Account durchführen. Beim nächsten Besuch der Web-App (innerhalb der Gültigkeitsdauer der Session) seitens dem Location-Betreiber, würde sich dieser in einem fremden Account befinden, was nicht unmittelbar ersichtlich ist. Alle Eingaben wären für den Angreifer sichtbar. Heruntergeladene QR-Codes würden dem Angreifer gehören.

Ein kurzer WTF-Moment am Rande

Der Login-Request unter /api/v3/auth/login hat zwei Felder. username und password. Wenn man ein falsches Passwort übermittelt, antwortet der Server mit 401 Forbidden. Wenn man das Passwort-Feld komplett entfernte und nur den Username übermittelte, antwortete der Server mit 200 OK. Zudem wurde ein Session-Cookie gesetzt. Dies war ein schöner WTF-Moment, da ich zunächst dachte, ich hätte nun eine gültige Session erhalten. Stattdessen erhielt ich eine Session mit einem ungültigen State, wodurch alle Anfragen (innerhalb der Gültigkeitsdauer der Session) mit einem 500 Internal Server Error beantwortet wurden. In Verbindung mit der CSRF-Lücke, konnte also zudem die Nutzung der Anwendung vorübergehend verhindert werden, da auch ein Logout nicht möglich war. Das ist natürlich unschön, aber nicht wirklich eine Sicherheitslücke.

Das Spektakel in einem Video

Kommunikation mit der culture4life GmbH

Für sicherheitsrelevante Dinge hat der Betreiber eine separate Mail-Adresse eingerichtet, welche auf der Seite des Bug Bounty Programms zu finden ist. Ob eine derartige Lücke „schlimm genug“ für eine Belohnung ist, ließ sich aus der Beschreibung des Bug Bounty Programms nicht entnehmen. Dennoch meldete ich die Lücke umgehend den Betreibern. Bereits am Morgen des nächsten Tages wurde diese behoben. Auch ein Bounty gab’s.

  • 08.04.2021 22 Uhr: Meldung der Lücke(n) an den Betreiber
  • 09.04.2021 10 Uhr: Betreiber meldet dass die Lücke(n) geschlossen wurden

Der Fix

Den Fix konnte man schnell bestätigen. Zu dem Zeitpunkt war der Backend-Code zwar noch nicht Open Source, ein kurzer Test zeigte aber, dass nun der Origin-Header aus dem Request validiert wird. Zudem wird nur noch application/json als Content-Type akzeptiert.

Die hinzugefügte Middleware restrictOrigin ist hier zu finden.

Fazit

Gerade beim Thema CSRF-Protection sollte man lieber eine Schutzmaßnahme mehr als zu wenig einbauen. Da wurde nun nachgebessert. Zudem hat der Betreiber sehr schnell und vorbildlich reagiert.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.