├── abandoned_issues.md
├── cve-luca
├── cve-2021-33838.md
├── cve-2021-33839.md
└── cve-2021-33840.md
└── luca_tracking.md
/abandoned_issues.md:
--------------------------------------------------------------------------------
1 | # Luca Auffällikeiten / Issues
2 |
3 | _Die Arbeit an diesem Dokument wurde vor Fertigstellung (am 19.06.2021) eingestellt (Details im letzten Abschnitt)_
4 |
5 | Author: Marcus Mengs (MaMe82)
6 |
7 | Liste von Auffälligkeiten/Problemen mit potentieller Sicherheitsrelevanz in der "Luca"-Fachanwendung.
8 |
9 | Versionen zum Betrachtungszeitpunkt
10 |
11 | - Backend/Web Services aus `Web` Repository: v1.1.11 (zwischenzeitlich 1.1.12 bis 1.1.16, später 1.2.0 bis 1.2.0)
12 | - AndroidApp: v1.7.4
13 |
14 | Die Ausführungen **haben keinen Anspruch auf Vollständigkeit** und werden ggf. nach Veröffentlichung ergänzt. Das Dokument unternimmt keine Anstregungen Termini, prozessuale oder funktionale Sachverhalte zu erläutern, die sich aus dem veröffentlichten Material zu "Luca" ergeben (Dokumentaion, Quellcode). Das Dokument nimmt an, dass LeserInnen mit dem System vertraut sind.
15 |
16 | # Referenzmaterial
17 |
18 | 1. [Kurzanalyse Netzwerkverkehr (Erstellung Bewegungsprofile)](https://github.com/mame82/misc/blob/master/luca_traceIds.md)
19 | 2. [Youtube Playlist mit Erlaeuterungen zu diversen Problemstellungen](https://www.youtube.com/playlist?list=PLKuX6iczGb3kuDsm2RFgbmRkTugkR9-UE)
20 | 3. [Demo Video "CSV Injection" Gesundheitsamt (Download-Link, da Youtube Video entfernt)](www.cyberawareness.de/public/luca_attack5.mp4)
21 |
22 | # 1. Erläuterungen zu Nutzerdaten im Backend (Auszug)
23 |
24 | ## 1.1 Verschlüsselte Kontaktdaten "Encrypted User Data"
25 |
26 | Die Kontaktdaten des Nutzers werden als JSON-String serialisiert und mit AES128 (Counter Mode) verschlüsselt dauerhaft auf dem Server abgelegt, sie umfassen:
27 |
28 | - numerische Version der Kontaktdaten (2 für App-Nutzer, 2 für Schlüsselanhänger)
29 | - Vorname
30 | - Nachname
31 | - Telefonnummer
32 | - Email
33 | - Straße
34 | - Hausnummer
35 | - Postleitzahl
36 | - Stadt
37 | - Verification Secret (nur für Schlüsselanhänger)
38 |
39 | Der Schlüssel zu den Kontaktdaten (`data secret`) ist für App-Nutzer nur Lokal bekannt, wird aber in der "verschlüsselten Kontaktdaten Referenz" bei jedem Location Check-In übermittelt (vergleiche Abschnitt "Check-In-Traces"; erlaubt dem Gesundheitsamt die Entschlüsselung von Kontaktdaten nach Zustimmung eines Location-Operators).
40 |
41 | Neben der Möglichkeit, das `data secret` aus Location Check-Ins zu generieren (nach Zustimmung des Location-Operators), kann ein berechtigtes Gesundheitsamt den Schlüssel direkt vom Nutzer erhalten, wenn dieser ihn bereitstellt (im Rahmen des gesonderten Verfahrens zur Bereitstellung der Nutzer Location Historie an das Gesundheitsamt).
42 |
43 | **Das `data secret` für Schlüsselanhänger leitet sich aus der Seriennummer des Anhängers ab. Die Offenlegung einer Seriennummer ist also gleichbedeutend mit der Offenlegung des Crypto-Schlüssels zu den zentral gespeicherten Kontakdaten. Die Seriennummern der bisher produzierten Schlüsselanhänger, wurden durch das gleiche Unternehmen vergeben, welches auch das Luca-System entwickelt (Nexenio). Es ist daher davon auszugehen, dass diese bekannt sind.**
44 |
45 | Die verschlüsselten Kontaktdaten werden als Base64-String mit maximaler Länge von 1024 Zeichen gespeichert. Es lassen sich hier also **beliebige Sequenzenzen** aus 768 Bytes AES verschlüsselte Daten speichern, die vollständig vom Nutzer kontrolliert werden (input). Eine Validierung dieser Eingabedaten, kann (und muss) nur im "Health Department Frontend" erfolgen, da die Daten im Normalfall erst beim Gesundheitsamt entschlüsselt werden.
46 |
47 | Zu den verschlüsselten Kontaktdaten wird eine Signatur angelegt. **Diese Signatur wird nicht über validierte Kontaktdaten gebildet, sondern über die "rohen" Binärdaten, welche verschlüsselt werden (also 768 Bytes, die beliebig wählbar sind)**
48 |
49 | ## 1.2 Check-In-Traces
50 |
51 | Ein `Trace` bildet im Luca-System den Besuch eines Nutzers in einer Location ab, er umfasst u.a.
52 |
53 | - Check-In Zeitpunkt
54 | - Check-Out Zeitpunkt (wenn bereits erfolgt)
55 | - `TraceID` (zur eindeutigen Identifikation eines Traces; Nutzer-pseudonym, da mittels Einwegfunktion aus einzigartigem `tracing secret` des Nutzers und minutengenauem Zeitstempel generiert)
56 | - Geräte-Typ (iOS App, Android App oder Schluesselanhänger)
57 | - Location ID (verweist auf Datensatz mit **unverschlüsselten** Daten der Location)
58 | - **verschlüsselte** "Kontaktdatenreferenz" (`encrypted contact data reference`)
59 | - setzt sich zusammen aus `UserID` eines registrierten Nutzers und dem `data secret` des Nutzers (der symmetrische Schlüssel zur Entschlüsselung der Kontakdaten des Nutzers, welche bei Registrierung persistent in der Backend-Datenbank abgelegt werden)
60 | - zweifach AES128 verschlüsselt
61 | - der innere Schlüssel wird aus dem asymmetrischem `Daily Key Pair` der Gesundheitsämter (gleich **für alle Gesundheitsämter**) mittels DLIES abgeleitet
62 | - für Schlüsselanhänger (Badges) kommt bei der inneren Verschlüsselung nicht der `Daily Key Pair` der Gesundheitsämter zum Einsatz, sondern das sogenannte `Badge Key Pair` (ein Schlüsselpaar, welches **nicht täglich rotiert**, aber durch die Gesundheitsämter erstellt wird)
63 | - der äußere Schlüssel wird aus dem asymmetrischem Schlüsselpaar des jeweiligen `Location-Operators` mittels DLIES abgeleitet (Wichtig: Dieses asymmetrische Schlüsselpaar existiert nicht je Location, sondern es ist **für alle LocationGroups und Locations, welche der Location-Operator verwaltet, gleich**)
64 | - "additional Trace Data" (optional, durch Location-Operator entschlüsselbar)
65 | - AES128 verschlüsselt
66 | - der Schlüssel wird aus dem asymmetrischem Schlüsselpaar des jeweiligen `Location-Operators` mittels DLIES abgeleitet (Wichtig: Dieses asymmetrische Schlüsselpaar existiert nicht je Location, sondern es ist **für alle LocationGroups und Locations welche der Location-Operator verwaltet gleich**)
67 | - "additional Data" können von Location-Betreibern zusätzlich optional erhoben und entschlüsselt werden. Diese Daten gehen aber auch in die Ergebnisse der Location-Abfragen durch Gesundheitsämter ein (z.B. in die Datenexporte). Darüber hinaus kommt dieser Datenansatz für sogennante `Private Treffen` zum Einsatz, bei denen der Vor- und Nachname des Gastes als "additional Data" zum Check-In codiert wird und so vom Gastgeber entschlüsselt und dargestellt werden kann.
68 | - Ob und welche Zusatzdaten von einer Location erhoben werden sollen, wird für jede Location in einem Schema festgehalten (API Endpunkt `api/v3/locations/additionalDataSchema/{locationId}`).
69 |
70 | ### 1.2.1 Ergänzungen zu "additional Trace Data" für Location Betreiber (Bestandteil der Checkin-Traces)
71 |
72 | Die "additional Trace Data" werden als Base64-String mit einer maximalen Länge von 4096 Zeichen gespeichert. Es lassen sich hier also **beliebige Byte-Sequenzenzen** von bis zu 3072 Bytes AES verschlüsselte Daten speichern, welche vollständig vom eincheckenden Nutzer kontrolliert werden (Input). Eine Validierung dieser Eingabedaten kann (und muss) an jeder Stelle erfolgen, an der die Daten entschlüsselt werden (Input) und in der Folge verarbeitet und ausgegeben werden (Output). Solche Stellen sind beispielsweise:
73 |
74 | 1. Frontend für Location-Betreiber (bisher werden hier "additional Data" nicht angezeigt, es wurde aber eine Funktionalität hinzugefügt, welche Tischnummern als "additional Data" führt und im Location-Frontend verarbeitet und darstellt.)
75 | 2. App, bei privaten Treffen (für private Treffen werden Ersteller des Treffens Vor- und Nachname der Gäste in der App angezeigt. Die dargestellten Daten basieren auf den "additional Data" welche der Gast - in Form eines 3072 Byte großen Blocks beliebiger Daten - beim Einchecken bereitstellt)
76 | 3. **Health Department Frontend** (hier werden neben den Kontaktdaten der Gäste einer Location auch alle "additional Data", welche die Gäste bereitgestellt haben, verarbeitet)
77 |
78 | # 2. Problemstellungen
79 |
80 | ## 2.1 [Backend] - IP Protokollierung
81 |
82 | Das Backend verwendet als Schutzmechanismus u.a. IP-basiertes Rate-Limiting. Der Ansatz bietet ein niedrigschwelliges Hindernis für AngreiferInnen mit durchschnittlichen Fähigkeiten (limitiert u.a. maximale Check-Ins, maximale Anzahl Nutzerregistreirungen usw., welche jeweils von der gleichen IP-Adresse ausgehen). Dem gegenüber stehen die Risiken, die durch die **flüchtige** Speicherung der Request-IPs (nicht pseudonymisiert/gehasht) - in Verbindung mit dem genauen angefragten Endpunkt und Zeitpunkt des jeweils letzten Zugriffs auf diesen Endpunkt (ableitbar aus "expiry time") - in einer Redis DB einhergehen.
83 |
84 | Entstehende Privacy-Risiken könnten bereits durch ein Hashing der IP-Adresse gemildert werden.
85 | Entstehende Möglichkeiten zur zeitlichen Korrelation der Redis-Datensätze (IP-Adressen, Endpunkt, Timestamp als Expiry Time), zu weiteren mit Zeitstempeln versehenen Informationen (beispielsweise pseudonymisierte Check-Ins), wären durch ein "künstliches Bias" in den "Expiry Times" machbar.
86 |
87 | ### Hinweis:
88 |
89 | Analog zum IP-basierten Rate-Limiting Code, existiert Code zum Telefonnummern-basierten Rate Limiting (jeweils Mobil und Festnetz).
90 | Die Funktionalität kommt (bisher) nur für Festnetznummern zum Einsatz, im Gegensatz zu IP-Adressen werden Telefonnummern vor Speicherung gehasht (SHA256).
91 |
92 | 1. [Code Refernz: IP-Logging](https://gitlab.com/lucaapp/web/-/blob/6f426776c9e569fc21e0bd29a52092803e21fa60/services/backend/src/middlewares/rateLimit.js#L19)
93 | 2. [Code Referenz: **einer** Datenspeicherung in Redis](https://gitlab.com/lucaapp/web/-/blob/6f426776c9e569fc21e0bd29a52092803e21fa60/services/backend/src/middlewares/rateLimit.js#L74)
94 | 3. [Code Referenz: Speicherung gehashter Telefonnummern (Keying)](https://gitlab.com/lucaapp/web/-/blob/6f426776c9e569fc21e0bd29a52092803e21fa60/services/backend/src/middlewares/rateLimit.js#L26)
95 | 4. [Code Referenz: Speicherung gehashter Telefonnummern (Speicherung)](https://gitlab.com/lucaapp/web/-/blob/6f426776c9e569fc21e0bd29a52092803e21fa60/services/backend/src/middlewares/rateLimit.js#L95)
96 | 5. [Code Referenz: Speicherung gehashter Telefonnummern (Anwendung)](https://gitlab.com/lucaapp/web/-/blob/6f426776c9e569fc21e0bd29a52092803e21fa60/services/backend/src/routes/v3/sms.js#L53)
97 |
98 | ### Auswirkungen
99 |
100 | Es wurden bereits anderweitig Möglichkeiten aufgezeigt, die es einem Backend-Beobachter (z.B. berechtigtes Betriebspersonal oder Threat-Actor mit Zugriff) ermöglichen, durch längere Beobachtung der Kommunikation folgende Ableitungen zu treffen:
101 |
102 | - Zuordnung aller Check-Ins/Check-Outs zu einzelnen Mobilgeräten
103 | - Zuordnung der Telefonnummer zu den Mobilgeräten
104 | - Zuordnung von Abfragen der Gesundheitsämter zu den jeweiligen Mobilgeräten
105 |
106 | (Vergleiche dazu Referenzmaterial 1 und 2)
107 |
108 | Durch die Speicherung von IP-Adressen mit ableitbaren Zugriffszeiten ergeben sich ähnliche Möglichkeiten auch **ohne längerfristige Beobachtung des Backends bei einmaligem Datenzugriff**. Dies ist insbesondere möglich, da die Datenlage zu einer Vielzahl an logischen Ereignissen (Nutzer-Registrierungen, Nutzer-Check-In/-Check-Out, Location-Registrierung etc.) ebenfalls mit unverschlüsselten Zeitstempeln gespeichert wird.
109 |
110 | ## 2.2 [Backend API] Unauthenticated access EP `/v3/locations/traces/{accessId}`
111 |
112 | - Der Endpunkt erlaubt Abfrage von Traces einer Location ohne Authentifizierung als Operator
113 | - Benötigt wird zur Abfrage die `AccessID` der Location
114 | - Diese wird bei der Registrierung der Location automatisch generiert (durch PostgresDB, beim Anlegen des Datensatzes)
115 | - Die Verwendung der `AccessId` als Query-Parameter (ohne weitere Authentifizierung), birgt das Risiko der ihrer Offenlegung (bspw. bei Person-in-the-Middle-Angriffe im WiFi-Netzwerk einer Location oder beim Logging des Query-Path an Intermediaries in komplexeren Enterprise IT-Netzen/ggf. Übermittlung von Queries an externe Security Services, etc.)
116 | - Abfragbare Informationen für Locations mit bekannter gewordener `AccessId`
117 | - individuelle Check-In- / Check-Out-Zeitpunkte
118 | - Typ des Checkins (Android-App / iOS-App / Schlüsselanhänger)
119 | - verwendete TraceID; die **TraceID ist Nutzerpseudonym**, verschiedene Nutzer können nie gleiche TraceIDs generieren (vgl. Referenzmaterial 1 und 2). Für Schlüsselanhänger (Badges), zu denen der QR-Code (Tracing Seed) oder die Seriennummer (erlaubt Ableitung aller geheimen Nutzerschlüssel) bekannt geworden ist, **ist eine direkte Nutzer-Zuordnung der TraceIDs möglich**.
120 |
121 | ## 2.3 [Backend, Health Department Frontend] Fehlende Plausibilitätsprüfungen (exemplarisch)
122 |
123 | Eine Plausibilitätsprüfung der durch Nutzer und Location-Betreiber eingebrachten Daten kann technisch bedingt oft nicht zentral, sondern nur nach Entschlüsselung von Daten erfolgen. Das heißt:
124 |
125 | - Für "encrypted user data" (vgl. Abschnitt 1.1) kann die Plausibilitätsprüfung erst im Frontend des Gesundheitsamtes erfolgen (Anteil des Luca-Systems, ausgelegt als Web-App, Datenentschlüsselung erfolgt lokal im Browser)
126 | - Für "additional Trace Data" (vgl. Abschnitt 1.2.1), ist eine Plausibilitätsprüfung möglich:
127 | - im Frontend des Gesundheitsamtes (nach Abruf der Daten von einer Location)
128 | - im Frontend des Location-Betreibers (z.B. beim Auswerten von Tischnummern, welche als "Additional Data" geführt werden; auch hier handelt es sich um einen Anteil des Luca-Systems, ausgelegt als WebApp, Datenentschlüsselung erfolgt lokal im Browser)
129 | - in der App, für private Treffen (Vor- und Nachname der Gäste werden als "additional data" geführt)
130 |
131 | Weiter bestehen bis heute Probleme bei der Verifizierung der Telefonnummern von Nutzern. Diese wird clientseitig beim Nutzer durchgeführt und kann daher umgangen/übersprungen werden. Die weiteren Kontaktdaten (Name, Adresse, etc.) werden ohnenhin nicht auf Plausibilität geprüft.
132 |
133 | Hieraus ergeben sich folgende Probleme, welche die Sicherheit und Nutzbarkeit des Systems schwächen können:
134 |
135 | 1. Durch Einzelpersonen können beliebig viele Luca-Nutzer registriert werden. Falsche, aber auch bereits vorhandene, Kontaktdaten können mehrfach registriert und die so registrierten Nutzer dann in beliebige Locations eingecheckt werden. Da die Telefonnummer nicht verifiziert wird, ist es auch möglich, plausible Kontaktdaten fremder Personen über Check-Ins in (beliebige Locations) Infektionsketten einzubringen. Das einzige Datum, für das seitens Luca garantiert wird, dass es verifiziert sei, ist dabei die Telefonnummer, welche daher auch durch die Gesundheitsämter zur Kontaktaufnahme herangezogen werden muss. Angesichts der Tatsache, dass beliebige Telefonnummern mehrfach registriert werden können, leidet nicht nur die System-Sicherheit erheblich, sondern auch dessen Funktionalität.
136 | 2. Der gleiche Nutzer kann in mehreren Locations gleichzeitig eingecheckt werden. Festgestellt werden könnte dies (aus vorgenannten Gründen) nur im Gesundheitsamt, wenn genau dieser Nutzer "getracet" wird, also der Nutzer seine vollständige Location-Historie bereitstellt. Solche unplausiblen "Mehrfach-Check-Ins" könnten im Gesundheitsamt **nicht** festgestellt werden, wenn die Kontaktdaten des Nutzers im Rahmen der Abfrage einer Location entschlüsselt werden (= Location-Betreiber stellt Gästeliste bereit) - sofern nicht zufällig mehrere Locations abgefragt werden, in denen sich der Nutzer zeitgleich befand. Hieraus ergibt sich, dass Schwachstellen, die darauf aufbauen, dass manipulierte Kontaktdaten eines Nutzers (bspw. Schadcode) durch ein Gesundheitsamt verarbeitet werden, beliebig skalieren. Ein solches Angriffsszenario würde nur zum Erfolg führen, wenn das Gesundheitsamt eine Location abfragt, in der sich im Abfrage-relevanten Zeitraum ein solcher "Schadnutzer" befunden hat. Die Möglichkeit redundanter Check-Ins, erlaubt es Angreifern allerdings, bereits einen einzelnen Schadnutzer beliebig oft in beliebig vielen Locations einzuchecken, um die Erfolgschancen eines Angriffsversuchs zu steigern.
137 | 3. Aus Punkt 1 und 2 ergibt sich, dass Locations mit Check-Ins "geflutet" werden können (sowohl mehrfache Check-Ins des gleichen Nutzers, als auch durch verschiedene Nutzer, die - mangels funktionierender Telefonnummernverifikation - in beliebiger Anzahl durch einen Angreifer registriert werden können). Es wird behauptet, dass solche "ungültigen" Check-Ins vor Auswertung im Gesundheitsamt herausgefiltert werden. Tests haben gezeigt, dass diese Filterung auf Signaturen der Kontaktdaten der registrierten Nutzer basiert, d.h. **sie greift ausschließlich, wenn die bei Registrierung des Nutzers verwendeten Daten nicht im vorgesehenen technischen Prozess verschlüsselt und signiert wurden. Die Signatur wird dabei nicht über validierte Kontaktdaten gebildet, sondern über Binärdaten, welche beliebigen Inhalt transportieren können (vgl. 1.1, letzter Absatz)**. Darüber hinaus ist es in vielen Angriffsszenarien unerheblich, ob die beworbene Datenfilterung überhaupt funktional ist, denn: Zur Umsetzung der Filterung selbst müssen (unter Umständen schadhafte) Kontaktdaten im Gesundheitsamt verarbeitet werden.
138 |
139 | ## 2.3.1 Ergänzung Plausibilität: Locations, die keine verwertbaren Daten produzieren (Ergänzung)
140 |
141 | Es häufen sich Berichte über Locations, welche Luca als Check-In-System anbieten, aber im Zuständigkeitsbereich von Gesundheitsämtern liegen, welche nicht oder noch nicht an Luca angebunden sind.
142 |
143 | Die Luca-Webseite bietet einen [Postleitzahlen-basierten Verfügbarkeitstest (link)](https://www.luca-app.de/nutzeluca/) an. **Dennoch wird beim Erstellen/Registrieren von Locations im Luca-System nicht geprüft, ob zu der für die Location angegebenen Postleitzahl ein zuständiges Gesundheitsamt an das System angebunden ist.** Die Unterlassung dieser Plausibilitätsprüfung führt offenbar dazu, dass gehäuft Location-Betreiber Luca als **einzige** digitale Checkin-Lösung anbieten, obwohl das zuständige Gesundheitsamt die Gästelisten nie abrufen könnte (mangels Anbindung). Werden die Gästedaten durch den Location-Betreiber nicht zusätzlich in anderer Form erfasst, kommt dieser in der Regel seiner Verpflichtung gemäß geltender CoronaSchVO nicht nach. Der Erhebungszweck der Daten durch Luca dürfte dabei ebenfalls in Frage gestellt sein.
144 |
145 | ## 2.4 [Location Frontend] Permanentes Vorhalten des Privaten Schlüssels des Location Operators in der Browser Session
146 |
147 | Beim Registrieren eines "Location Operators" (Betreiber einer oder mehrerer Locations), wird ein asymmetrisches Schlüsselpaar erstellt, für welches der private Schlüsselanteil im Registrierungsprozess einmalig zum Download angeboten wird. Dieser private Schlüssel ist nochmals mit AES128 umschlüsselt (encrypted private key). Der zugehörige Entschlüsselungsschlüssel (`private key secret`) ist im Backend gespeichert und kann nur mittels authentifizierter Session des `Location Operators` abgerufen werden.
148 |
149 | Der private Schlüssel des Location-Betreibers kann u.a. dazu genutzt werden, die "additional Trace Data" eingecheckter Gäste zu entschlüsseln (vgl. Abschnitt 1.2 und 1.2.1) oder die äußere Verschlüsselung der "encrypted contact data reference" (vgl. Unterpunkt in Abschnitt 1.2) aufzuheben.
150 |
151 | Die Möglichkeit, diese Entschlüsselungen vorzunehmen besteht für einen authorisierten "Location-Operator" ohnehin, allerdings nicht für Angreifer, die in der Lage wären, durch die Asunutzung von Sicherheitslücken die Browser-Session zu übernehmen. Dies begründet sich darin, dass das "Location Frontend" - auch wenn kompromittiert - in einer Browser-Sandbox läuft, aus welcher heraus ein Zugriff auf den (als lokale Datei gespeicherten) privaten Schlüssel des Location Operators nicht ohne weiteres möglich ist.
152 |
153 | Bisher wurde der private Schlüssel des "Location-Operators" nur im Bedarfsfall in den Browser geladen, nämlich dann, wenn eine Abfrage der Gäste-Check-Ins durch ein Gesundheitsamt erfolgt ist und der Location-Operator dieser Anfrage nachkommt. Das Risiko hierbei ist überschaubar, da der private Schlüssel nur temporär im Browser-Kontext verfügbar war.
154 |
155 | Mit einem Update der Web-Services Anfang Mai (v1.1.8), wurde die Funktionalität des Location-Frontends allerdings so angepasst, dass der Location-Operator beim Login aufgefordert wird, seinen privaten Schlüssel in die Browser-Session zu laden.
156 |
157 | Dies schwächt die Schlüssel-Sicherheit im Kontext möglicher Angriffe auf den Browser immens. Als Grund für diese Maßnahme wurde angeführt, dass der Location-Operator nur so die Tischnummern der Gäste-Checkins entschlüsseln kann (diese werden als "additional data" im Check-In Trace hinterlegt).
158 |
159 | ### Bewertung:
160 |
161 | Die Funktionalität, Tischnummern auf Basis des permanent in den Browser geladenen **privaten Schlüssels** des Location-Operators anzuzeigen, steht im Missverhältniss zu den entstehenden technischen Risiken. Zunächst hat der private Schlüssel nicht nur Bezug zu einer Einzel-Location, sondern gilt global für alle Locations des Location-Operators. Darüber hinaus wäre die Segmentierung eines Veranstaltungsortes in Zonen (wie "Tische") auch ohne die Verwendung des "additional data"-Konstruktes möglich: Das System verwaltet bereits LocationGroups, welche mehrere Locations zusammenfassen; im Frontend sind LocationGrous als "Location" benannt, wobei die eigentlichen Locations als "Area" benannt werden.
162 |
163 | Die Funktionalität der "additional data", welche durch Location-Betreiber ohne das Zutun des Gesundheitsamts entschlüsselt werden können, ist ohnehin höchst fragwürdig und bietet Missbrauchspotential. Eines der möglichen Angriffsszenarien auf "additional trace data" wird im nächsten Abschnitt dargestellt.
164 |
165 | ## 2.5 [App, Backend] Ausnutzung "additional Trace Data", Erschleichen von Nutzerdaten durch Location-Provider
166 |
167 | Die Abschnitte 1.2 und 1.2.1 sprechen "additional Trace Data" als Konstrukt an, welches es ermöglicht, an **jeden Nutzer-Check-In** weitere beliebige Daten zu koppeln, welche nicht für das Gesundheitsamt verschlüsselt werden, sondern bereits durch den Location-Betreiber entschlüsselt werden können (ohne Zutun eines Gesundheitsamtes).
168 |
169 | Es ist vorgesehen, dass der Location-Betreiber beim Erstellen von Locations festlegt, welche Daten er zusätzlich erhebt. Die Daten sollen aus Key-Value-Paaren bestehen. Über die Struktur der für eine Location zusätzlich zu erhebenden Daten wird ein Schema angelegt (vgl. Abschnitt 1.2).
170 |
171 | Aus funktionaler Sicht werden die vom Betreiber vorgesehenen Zusatzdaten ("additional data") nur abgefragt, wenn der Locationbetreiber selbst seine Gäste mittels des Kontaktformulares erfasst, welches das Luca-System als zusätzliche Check-In-Möglichkeit bereitstellt. Ein Self-Check-In mit der App führt derzeit bspw. nicht zu einer Abfrage dieser Daten (dies könnte anndernfalls durch den Location-Betreiber missbraucht werden, um zusätzliche persönliche Daten von eincheckenden Gästen zu "erschleichen").
172 |
173 | Dennoch kommt das angelegte Schema nicht zur Plausibilitätsprüfung der "additional data", die ein Gast beim Einchecken bereitstellt, zum Einsatz. So ist es z.B. möglich, als Gast "additional data" an einen Check-In anzuhängen, obwohl dies von der Location gar nicht vorgesehen ist. Überall, wo diese Zusatzdaten ungefiltert verarbeitet werden, entsteht daher zusätzliche Angriffsfläche (wird in anderen Abschnitten behandelt).
174 |
175 | Eine riskante Konstelation ergibt sich im Zusammenhang mit den sogenannten "private Meetings". Es handelt sich dabei um Treffen, welche von Luca-App Nutzern geöffnet werden können. Gäste können dann durch Scannen des QR-Codes des Gastgebers bei dem "private Meeting" einchecken. Das private Meeting unterscheidet sich (Backend-seitig) in der technischen Gestaltung nur in einem Punkt von einer regulären Location: Der Datensatz der Location hat das Attribut `isPrivate` als `true` hinterlegt (siehe [link](https://gitlab.com/lucaapp/web/-/blob/af4bdb2a3af8d2b5d46ef8f27d69eff59516710a/services/backend/src/database/models/location.js#L87)).
176 |
177 | Für eincheckende Gäste unterscheidet die App zwischen "private Meetings" und "echten Locations" wiederum anhand des **Aufbaus der Location URL** im QR-Code des Meetings/der Location. Der eigentliche Check-In-Prozess und die damit einhergehende Datenverschlüsselung läuft für "private Meetings" und Self-Check-Ins in "echten Locations" analog. Es existiert nur ein entscheidender Unterschied: Für **alle Locations, deren QR-Code den Aufbau eines privaten Meetings hat, sendet die App den Vor- und Nachnamen des Gastes als "additional data" mit (d.h. für Locationbetreiber entschlüsselbar)**. Der Übermittlung der Daten geht ein kurzer Informationsdialog voran, in dem darauf hingewiesen wird, dass für private Meetings Vor- und Nachname übermittelt werden.
178 |
179 | Aufbau der Checkin-URL für eine reguläre Location:
180 |
181 | ```
182 | https://app.luca-app.de/webapp/{scannerID}#...
183 | ```
184 |
185 | Aufbau der Checkin-URL für ein privates Meeting:
186 |
187 | ```
188 | https://app.luca-app.de/webapp/meeting/{scannerID}#...
189 | ```
190 |
191 | **Da die App an keiner Stelle überprüft, ob es sich bei der Ziellocation tatsächlich um ein "privates Meeting" handelt, erfolgt die übermittlung von Vor- und Nachnamen des Gastes als "additional data" auch an Locations, welche zwar kein privates Meeting sind, dies aber in ihrem QR-Code so angeben.** Mehr noch: Der Check-In-Trace (welcher nun Vor- und Nachname des Nutzers als "additional data" enthält) wird vom Backend-Server selbst dann gespeichert, wenn das für die Location laut Schema gar keine "additional data" anfallen dürften. Der Server könnte bei Speicherung zwar nicht auf Plausibilität der "additional data" gegenüber dem Schema prüfen (Daten sind nur durch Locationbetreiber zu entschlüsseln), aber sehr wohl feststellen, dass für ein Schema, welches keine zusätzlichen Daten fordert, auch keine zusätzlichen verschlüsselten Daten zu speichern sind.
192 |
193 | Mit einer minimalen Veränderung am QR-Code für Self-Check-Ins kann ein Location-Betreiber also die automatisierte Übermittlung von Vor- und Nachnamen eincheckender Gäste auslösen. Die Gäste werden zwar mit einem Informationsdialog konfrontiert, der aussagt, dass für "Private Meetings Vor- und Nachnamen an den **Meeting Gastgeber** übermittelt werden". Der Hinweis macht im Kontext einer Location wenig Sinn, dürfte aber ohnehin von vielen Nutzern ignoriert werden, die einchecken möchten.
194 |
195 | Der Angriff wurde hier demonstriert: [Youtube-Link](https://youtu.be/jWyDfEB0m08).
196 |
197 | Die Problemstellung ist dem Hersteller [bekannt](https://twitter.com/patrick_hennig/status/1387738281757061125).
198 |
199 | Die Problemstellung wird von [CVE-2021-33839 (link)](https://github.com/mame82/misc/blob/master/cve-luca/cve-2021-33839.md) erfasst.
200 |
201 | ## 2.6 [Backend, App] Durch Beobachtung von Netzwerkverkehr können Bewegungsprofile erstellt und mit Nutzerdaten verknüpft werden
202 |
203 | Durch **reine Beobachtung des Netzwerkverkehrs am Backend**, lassen sich **ohne Kenntnis des Schlüsselmaterials für Kontaktdaten oder für Check-In-Traces** folgende Ableitungen treffen:
204 |
205 | - Check-In-Historie für das Mobilgerät von teilnehmenden App-Nutzern lässt sich rekonstruieren
206 | - für rekonstruierte Check-In-Historien lässt sich die Telefonnummer als persönliches Identifikationsmerkmal zuordnen
207 | - für rekonstruierte Check-In-Historien lässt sich zuordnen, ob Check-Ins durch ein Gesundheitsamt im Rahmen der Nachverfolgung einer Infektionskette abgefragt wurden (in der Regel ist die entsprechende Check-In-Historie damit infektionsrelevant).
208 |
209 | Ursächlich sind hier:
210 |
211 | 1. Hohe Abfragefrequenz von Check-Ins durch die App am Backend (bis zu 20-mal pro Minute), unter wiederholter Verwendung gleicher `TraceIDs` welche **nur von einem Gerät im System stammen können (pseudonym)**. Hierdurch wird die Korrelation verschiedener Check-Ins zum gleichen Gerät möglich, selbst wenn das Gerät die IP-Adresse wechselt oder die Nutzung der App unterbrochen wird (auch über einen Geräteneustart hinweg werden `TraceIDs` wiederholt abgefragt).
212 | 2. Die zusätzlich erzwungene Übermittlung von weiteren Meta-Informationen für **jeden** Request der App zum Backend (Gerätehersteller, Gerätemodell, Betriebssystemversion). Im Grunde genügen die wiederholt verwendeten TraceIDs, um Requests zu einem Gerät zu korrelieren. Die ergänzenden Metadaten geben allerdings weitere Korrelationsmöglichkeiten für Requests der App, welche selbst keine TraceIDs enthalten (u.a. lässt sich der Request zur Übermittlung der Telefonnummer bei der Registrierung so korrelieren). Für mobile Datenverbindungen ist es häufig der Fall, dass der Mobilfunkanbieter mehrer Geräte hinter einer IP-Adresse "kaskadiert". Die Übermittlung ergänzender Gerätedaten genügt allerdings, um diese Kaskadierung mit hoher Genauigkeit wieder zu Einzelgeräten aufzulösen (es existieren 3 Classifier mit hoher Entropie, während nur wenige Geräte hinter einer IP-Adresse kaskadiert werden).
213 |
214 | Das Problem ist seit langem bekannt und wurde mehrfach kommuniziert. Bestrebungen zur Mängelbeseitgung waren bisher nicht erkennbar.
215 | Ausführliche Beschreibungen, siehe: "Referenzmaterial Nr. 1" und "Referenzmaterial Nr. 2".
216 |
217 | Die Problemstellung wird von [CVE-2021-33838 (link)](https://github.com/mame82/misc/blob/master/cve-luca/cve-2021-33838.md) erfasst.
218 |
219 | ## 2.7 Keine verifizierten Kontaktdaten für Gesundheitsamt (Umgehen der Telefonnummern-Verifikation)
220 |
221 | Die Kontakdaten der Luca-Nutzer werden weitestgehend **nicht verifiziert**. Die Ausnahme bildet hierbei die Telefonnummer, die bei der Registrierung durch den Nutzer angegeben wird. Die Telefonnummer stellt damit das einzige verlässliche Datum im Falle einer nötigen Kontaktaufnahme durch ein Gesundheitsamt dar.
222 |
223 | Die Telefonnummer-Verifizierung lässt sich jedoch umgehen, da deren erfolgreiche Durchführung keine Voraussetzung für die Registrierung eines Nutzers ist. Die Registrierung eines **gültigen Nutzers** kann nicht nur unabhängig von der SMS-TAN-Verifizierung erfolgen, sondern es sind auch beliebige Telefonnummern verwendbar.
224 |
225 | Die Problemstellung wird von [CVE-2021-33840 (link)](https://github.com/mame82/misc/blob/master/cve-luca/cve-2021-33840.md) erfasst.
226 |
227 | ## 2.8 Schlüsselanhänger (Badges)
228 |
229 | Es existieren derzeit zwei Versionen von Badges, welche sich im Umlauf befinden (V3 und V4).
230 |
231 | Es ist öffentlich nicht bekannt, wie viele der Badges der Version 3 im Umlauf sind. Aus Betreiber-Sicht ist dies allerdings auf mehreren Wegen feststellbar (z.B. sind die `UserIDs` der Version 3-Badges für das fünfte Byte wie folgt maskiert: `BadgeUserID[4] = BadgeUserID[4] & 0xf0`, siehe auch [Code link](https://gitlab.com/lucaapp/web/-/blob/14098e0683114479ddf3e399b79f788dc6b88e33/services/locations/src/components/RegisterBadge/RegisterForm/SerialCode/SerialCode.helper.js#L64)).
232 |
233 | Die Unterscheidung in Badges der jeweiligen Versionen ist in sofern relevant, als dass die Badges der Version 3 das gesammte Schlüsselmaterial mit vergleichsweise schwachen kryptografischen Hash-Funktionen erstellen. Dies soll kurz erläutert werden:
234 |
235 | Die Badges repräsentieren Nutzer (analog zu Nutzern, welche sich per Luca-App registrieren). Im Gegensatz zu App-Nutzern, können geheime Schlüssel nicht sicher auf dem Gerät eines Nutzers gespeichert werden, sondern werden in den Badges kodiert. Geheime Schlüssel sind unter anderem:
236 |
237 | - `tracingSeed`: Basis zur Ableitung des `tracing secret`. Aus dem `tracing secret` können **alle** `TraceIDs` generiert werden, welche für Check-Ins eines Schlüsselanhängers verwendet werden. Ist dieses Secret bekannt, können damit alle Locations, bei denen der Schlüsselanhänger eingecheckt war, abgeleitet werden (`TraceIDs` für Check-Ins und zugeordnete Locations werden im Backend unverschlüsselt gespeichert). Beliebige `TraceIDs` waren zeitweise ohne Authentifizierung von außen abfragbar, sodass Check-In-Historien für Schlüsselanhänger wiederhergestellt werden konnten (["LucaTrack"](http://lucatrack.de)). Mittlerweile werden API-Anfragen für Check-In-Daten zu `TraceIDs`, welche Schlüsselanhängern zugeordnet sind, nicht mehr beantwortet (vergleiche [code link 1](https://gitlab.com/lucaapp/web/-/blob/14098e0683114479ddf3e399b79f788dc6b88e33/services/backend/src/routes/v3/traces.js#L137), [code link 2](https://gitlab.com/lucaapp/web/-/blob/14098e0683114479ddf3e399b79f788dc6b88e33/services/backend/src/routes/v3/traces.js#L168)), dennoch liegen die entsprechenden Daten unverschlüsselt in der Postgres-Datenbank des Backends. **Das `TracingSeed`, welches zur Zuordnung der Check-In-Historie benötigt wird, leitet sich aus der Seriennummer eines Badges ab, ist aber auch Bestandteil des QR-Codes (welcher naturgemäß für Check-Ins gezeigt werden muss)**
238 | - `user data secret`: Der symmetrische AES128-Schlüssel zu den verschlüsselten Kontaktdaten des Nutzers, welche in der Datenbank des Luca-Backends gespeichert sind (`encrypted contact data`, vergleiche Abschnit 1.1). **Das `user data secret` leitet sich aus der Badge-Seriennummer ab und ist nicht Bestandteil des QR-Codes. Die Seriennummer ist aber auf Vorder- oder Rückseite des Badges aufgedruckt.**
239 | - `user key pair`: Aus der Seriennummer eines Badges leitet sich auch ein asymmetrisches Schlüsselpaar ab. Der öffentliche Schlüssel, der sich darazs ergibt, wird (neben der UserID) vom Backend als Auswahlkriterium für Nutzerdatensätze herangezogen.
240 | So war es beispielsweise möglich, für Badges der Version 3 mit bekannter Seriennummern die Kontaktdaten der Badge-Nutzer zu überschreiben, sofern ein Datensatz für den Public Key des `user key pair` vorhanden war (für existierende Badges ist dieser Datensatz immer vorhanden, für Badges der Version 4 war dies nicht durchführbar, da der komprimierte public key - also ein anderes Format - als Identifier in der Postgres-Datenbank diente). Die gemeldete Sicherheitslücke wurde in einem Gitlab Issue dokumentiert ([link](https://gitlab.com/lucaapp/web/-/issues/16)) und zum "error report" umdeklariert (betroffen war hier das IT-Sicherheits-Schutzziel "Integrität").
241 | Aufgrund des abweichenden Public Key Formates lässt sich durch den Betreiber auch feststellen, wie viele Badges der Version 4 im Einsatz sind (für die Datensätze von App-Nutzern, Nutzern die über Kontaktformulare angelegt wurden und Nutzern von Badges der Version 3 werden **unkomprimierte** public keys verwendet).
242 | **Das `user key pais` leitet sich aus der Badge-Seriennummer ab und ist nicht Bestandteil des QR-Codes. Die Seriennummer ist aber auf Vorder- oder Rückseite des Badges aufgedruckt.**
243 |
244 | Zusammenfassend kann man sagen: Das gesamte Schlüsselmaterial für Badges leitet sich aus der Seriennummer ab. Die Seriennummer selbst repräsentiert dabei die Base32-Crockford-kodierte Form eines 56Bit Zufallswertes (vom sogenannten "Badge Generator" festgelegt). Die Funktion des "Badge Generators" entfällt derzeit auf "neXenio" (also die Firma, die auch das Luca-System selbst entwickelt und Security-Issues handhabt), wie in einem weiteren Gitlab Issue dokumentiert wurde ([link](https://gitlab.com/lucaapp/web/-/issues/15)):
245 |
246 | ```
247 | ... Currently, there are 29,000 badges in use, we [neXenio] do not have a distinction in
248 | V3/V4 here. These are currently created by a member of the security team of neXenio on
249 | behalf of culture4life and luca and are directly transferred to the producer in NRW without
250 | any detours. ...
251 | ```
252 |
253 | Technische Maßnahmen, die eine missbräuchliche Nutzung - der beim "Badge Generator" bekannten Seriennummern aller Schlüsselanhänger - verhindern, sind derzeit nicht aus dem Quellcode ableitbar.
254 |
255 | Ungeachtet dessen wird **jedem**, dem der QR-Code eines Schlüsselanhängers zur Kenntnis kommt, die `TracingSeed` bekannt gemacht.
256 |
257 | Für Badges der Version 3 werden alle geheimen Schlüssel, ausgehend von der 56Bit-Seriennummer, auf Basis von SHA256-Hashes generiert. Erst für Badges der Version 4, kommt hier das wesentlich ressourcenaufwendigere `argon2` Hash-Verfahren zum Einsatz. Im Resultat sind **QR-Codes von Schlüsselanhängern der Version 3 anfällig für Brute-Force-Angriffe**.
258 |
259 | Der im QR-Code enthaltene `TracingSeed` leitet sich wie folgt aus der Seriennummer ab (`SerialBytes` als 56Bit Byte-Sequenz, also nicht Base32 codierte Form).
260 |
261 | ```
262 | // Die 7 Byte Seriennummer wird um das byte 0x02 ergänzt und mit SHA256 gehasht.
263 | // Vom resultierenden SHA256 Hash, werden die ersten 16 Byte als TracingSeed verwendet
264 | TracingSeed = SHA256(SerialBytes + (byte) 0x02).slice(0, 16)
265 | ```
266 |
267 | Nimmt man an `0x3f0ab086a8246e397ba2e202ee4bbd4a` repräsentiert das `TracingSeed`, welches aus dem offengelegten QR-Code eines Schlüsselanhängers der Version 3 extrahiert wurde, liese sich bereits mit Standard-Tools ein Angriff durch Laien starten, auch wenn nur 16 Bytes des SHA256-Hashes verwendet werden. Als Beispiel soll hier John-The-Ripper (JtR) dienen (in der Praxis würde man einen performanteren Ansatz wählen, der auch alle "Matches" für den gekürzten SHA256-Hash zurückliefert).
268 |
269 | **haslist.txt:**
270 |
271 | ```
272 | tracingseed1:3f0ab086a8246e397ba2e202ee4bbd4a
273 | ```
274 |
275 | **JtR Aufruf zum Bruteforce des partiellen SHA256-Hashes (liefert bei Erfolg nur ersten Treffer, verwendet nur die ersten 16 Byte des Hashes als Match-Kriterium):**
276 |
277 | ```
278 | john --mask='?b?b?b?b?b?b?b\x02' --fork=8 --format=dynamic_1029 hashlist.txt
279 | ```
280 |
281 | Führt ein solcher Bruteforce-Angriff zum Erfolg, ist das Ergebnis die Seriennummer des Schlüsselanhängers, aus der sich das gesammte Schlüsselmaterial ableiten lässt.
282 |
283 | Ein Bruteforce-Angriff wäre ebenfalls denkbar, indem man für "geratene" Seriennummern NutzerIDs ableitet und dann prüft, ob diese Nutzer existieren (API-Endpunkt `api/v3/users/{userId}`). Für Badges der Version 3 bleibt der Bruteforce-Aufwand dabei vertretbar (zeimaliges Hashen mit SHA256), erst Badges der Version 4 bauen hier Hürden durch Verwendung von `argon2` als Hash-Funktion auf. Das [IP-basierte Rate-Limit zur Abfrage von `userIDs`](https://gitlab.com/lucaapp/web/-/blob/14098e0683114479ddf3e399b79f788dc6b88e33/services/backend/src/routes/v3/users.js) stellt für Bruteforce-Angriffe kaum ein Hindernis dar (vgl. Abschnitt 2.1)
284 |
285 | ## 2.8.1 Plausibilität beim Schlüsselanhänger-Check-In wird unzureichend geprüft
286 |
287 | Ein QR-Code eines Schlüsselanhängers beinhaltet zusätzlich eine `Attestation Signature`, welche im Grunde eine Signatur der im QR-Code enthaltenen Daten darstellt. Diese Signatur basiert auf dem privaten Schlüssel des "Badge Generators" (Entität, welche die Schlüsselanhänger erstellt). Ein Angreifer, welcher die Seriennummer eines Schlüsselanhängers kennt, kann zwar (ohne Kenntnis des QR-Codes) das gesammte Schlüsselmaterial zu diesem Badge ableiten, nicht aber die `Attestation Signature`. Theoretisch könnte damit verhindert werden, dass ein Akteur, der die Badge-Seriennummner kennt, nicht aber den QR-Code (und die beinhaltete `Attestation Signature`), den Schlüsselanhänger wahllos in Locations eincheckt. In der Praxis wird die `Attestation Signature` aber **nur vom "Scanner-Frontend"** (also einem aktiven QR-Code-Scanner, den der Location Owner betreibt) **geprüft**, bevor das "Scanner-Frontend" den eigentlichen Check-In am entsprechenden API-Endpunkt vollführt. Der Backend-API-Endpunkt für Check-Ins nimmt keine weitere Signatur-Prüfung vor (und kann es technisch auch nicht). Ein Angreifer kann deshalb Badges, zu denen die Seriennnummer bekannt ist, ohne Probleme in beliebige Locations einchecken - ohne Kenntnis der `Attestation Signature`.
288 |
289 | Bemerkenswert an dieser Stelle:
290 |
291 | Die `Attestation Signature` ist unterschiedlich aufgebaut für Badges der Version 3 ([Link: V3 Signature check - Variante 1](https://gitlab.com/lucaapp/web/-/blob/14098e0683114479ddf3e399b79f788dc6b88e33/services/scanner/src/utils/qr.js#L53), [Link: V3 Signature check - Variante 2](https://gitlab.com/lucaapp/web/-/blob/14098e0683114479ddf3e399b79f788dc6b88e33/services/scanner/src/utils/qr.js#L59)) und der Version 4 ([Link: V4 Signature check](https://gitlab.com/lucaapp/web/-/blob/14098e0683114479ddf3e399b79f788dc6b88e33/services/scanner/src/utils/qr.js#L140)). Der Quellcode, welcher Entitäten in der Rolle eines "Badge Genrators" erlaubt hat, Badges der Version 3 zu generieren, ist allerdings nie veröffentlicht worden. Der API-Endpunkt zur Badge Generierung lässt in allen veröffentlichten Quellcode-Varianten ausschließlich Signaturen für V4 Badges zu (Signatur über `userId + encrypted contact data reference`, siehe auch [Link: Signature Check bei Badge Erstellung](https://gitlab.com/lucaapp/web/-/blob/14098e0683114479ddf3e399b79f788dc6b88e33/services/backend/src/routes/v3/users.js#L117)).
292 | Ebenso wurde der Source Code zur Badge-Erstellung durch "Badge Generator"-Entitäten nur für Badges der Version 4 veröffentlicht. Es lässt sich daher nicht sagen, ob der PRNG, der zur Generierung der Seriennummern von V3 Badges zum Einsatz kam, ggf. Mängel aufwies (die z.B. erlauben würden, gültige Seriennummern zu "erraten").
293 |
294 | ## 2.8.2 Schlüsselanhänger Designfehler (persönliche Bemerkung)
295 |
296 | Das Konzept der Schlüsselanhänger (oder "static badges") vereint - nach meiner persönlichen Meinung - mehr Designfehler allein auf sich, als der verbleibende ["Security Overview"](https://luca-app.de/securityoverview/intro/landing.html) abbildet.
297 |
298 | Allem voran ist hier die **Ableitung aller kryptologischen Geheimnisse aus einem 56 Bit Zufallswert - welcher durch den Nutzer nicht veränderbar ist und als Seriennummer auf den Schlüsselanhänger gedruckt wird -** zu nennen. Bedenkt man, wie die Schlüsselanhänger zu nutzen sind (Vorzeigen zum Scannen), ist diese Seriennummer kaum geheim zu halten.
299 |
300 | Eine bekannte gewordene Seriennummer erlaubt es, die mit dem Schlüsselanhänger verknüpften Kontaktdaten beliebig zu ändern (rückwirkend, da Kontaktdaten bei Änderungen systemweit überschrieben werden) und die Anhänger an beliebigen Orten einzuchecken (ein bekanntgewordener Badge-QR-Code genügt dafür auch). Da die "allmächtige" Seriennummer der Schlüsselanhänger durch den Nutzer nicht veränderbar ist, wird insbesondere dem ersten Problem (Änderung der Kontaktdaten), auf fragwürdige Art und Weise begegnet:
301 |
302 | - Das System verhindert das Lesen der hinterlegten Kontaktdaten durch die Besitzer der Schlüsselanhänger vollständig
303 | - Das Schreiben der Kontaktdaten ist nur **einmal** erlaubt (Registrierung des Anhängers durch Nutzer). Eine Fehlerbehebung oder die Aktualisierung der Kontaktdaten ist seitens Besitzer des Schlüsselanhängers nachträglich nicht mehr möglich.
304 |
305 | Es gab aber bereits einen Fehler, der die nachträgliche Veränderung der Kontaktdaten zu Schlüsselanhängern ermöglicht hat, hier haben die "Absicherungsmaßnahmen" dazu geführt, dass der tatsächliche Besitzer des Anhängers von dieser Änderung nichts bemerken konnte ([Demo Video, Youtube](https://youtu.be/WjV0nCAojg4), [Gitlab Issue/fix](https://gitlab.com/lucaapp/web/-/issues/16)).
306 |
307 | Kurz vor der Meldung des vorgenannten Sicherheitsproblems wurde außerdem festgestellt, dass mit Schlüsselanhängern vorgenommene Check-Ins durch Gesundheitsämter nicht auswertbar waren. Ursächlich war ein "kleiner" Programmierfehler: Für die Schlüsselanhänger-Erstellung und -Registrierung wurden Seriennummern anders dekodiert, als im Frontend des Gesundheitsamtes (Base32 vs. Base32-Crockford). Im Resultat wurde für Abfragen durch das Gesundheitsamt nicht nur ungültiges Schlüsselmaterial erzeugt, sondern auch ungültige NutzerIDs und TraceIDs. Für diese TraceIDs konnten im System gar keine Check-Ins abgefragt werden.
308 |
309 | Aus meiner Sicht erneut bemerkenswert: Wie der gemeldete Security-Issue (hier war erneut das Schutzziel der "Integrität", aber auch der "Verfügbarkeit" betroffen, [vergl. Kurzauszug aus der Meldung - Twitterlink](https://twitter.com/mame82/status/1392209069352574983)) abgearbeitet wurde:
310 |
311 | Auch hier wurde ein gemeldeter Security-Issue zu einem "Error Report" umdeklariert ([vgl. Gitlab Issue](https://gitlab.com/lucaapp/web/-/issues/15)). Da das Problem klar umrissen wurde, konnte es auch schnell behoben werden ([Update - Link Gitlab](https://gitlab.com/lucaapp/web/-/commit/2eb0c4687e65d0c1f095803cf1c21b9fcc5bd68e)). Allerdings fiel auch auf, dass der angepasste Code verzugsfrei am Produktionssystem integriert wurde - kein Rollout auf einem Referenzsystem, kein Staging, vor allem **keine Tests**. Dies mag für eine überschaubare Änderung vertretbar sein, dass eine solche Kernfunktion allerdings wochenlang unbemerkt ohne Funktion war, hat auch gezeigt: **Es gab hier nie funktionale Tests**.
312 |
313 | In dem zugehörigen [Gitlab Issue](https://gitlab.com/lucaapp/web/-/issues/15) wurde dazu folgende Bemerkung veröffentlicht:
314 |
315 | ```
316 | We followed up on this issue. We were able to determine that history retrieval via serial
317 | number was no longer possible due to the incorrect renaming of the Base32 Crockford function
318 | in one of the latest luca releases.
319 | ```
320 |
321 | Aus der Commit-Historie war ein solches `"...incorrect renaming of the Base32 Crockford function..."` nicht ersichtlich (zwischen dem Initial Release und diesem Patch lagen mittlerweile **10 Version-Patches**, davon 1 Minor Release Update, über eine Zeit von einem Monat Codeanpassungen). Der fehlerhafte Patch, der dieses Problem verursacht haben soll, ist vor dem Initial-Commit der Version `v1.0.0` für mich bis heute nicht erkennbar. Aus meiner persönlichen Sicht heißt dies: Es fehlt nicht nur an Tests für Patch-Deployments, sondern es finden auch **keine hinreichenden funktionalen Tests für Kernfuntionalitäten des Systems statt**. Das Kommunikationsverhalten bzw. "Acknowledgment" des Herstellers zu gemeldeten Problemen erscheint im Vergleich zu anderen Vendoren höchst fragwürdig. Ich selbst würde sogar soweit gehen zu sagen, dass das **Herstellerverhalten nicht nahelegt, dass man an der Verantwortung geachsen ist, die sich aus der Gestaltung des Luca-Systems (bezogen auf die Systemteilnehmer) ergibt**.
322 |
323 | # 3. Input filtering / Output filtering
324 |
325 | ## 3.1 Allgemeines
326 |
327 | Dieser Abschnitt bildet den Kernteil dieser Notizen, da hier aus meiner Sicht die offensichtlichsten und eklatantesten Mängel bestehen. Ich möchte diese exemplarisch am Beispiel des "Health Department Frontends" anhand der Kontaktdaten von Nutzern des Systems darstellen (Kontaktdaten sind nutzerkontrollierter Input). Es existieren auch an anderen Stellen Eingabedaten (Input), welche nach Verarbeitung im System wieder zu Ausgabedaten werden. Ob diese Ausgabedaten dann über definierte Schnittstellen an nachgeordnete Systeme weitergegeben werden (Export, Anbindung an externer APIs) oder vom Luca-System selbst verwendet werden (z.B. zur visuellen Darstellung oder System-internen Weiterverarbeitung) spielt keine Rolle, sofern man folgenden "common sense" Maßstab anlegt:
328 |
329 | 1. **Eingabedaten (Input) ist zu validieren**
330 | 2. **Ausgabedaten (Output) sind entsprechend des Kontextes, in dem sie verwendet werden, zu kodieren oder zu "escapen", um ungewünschte Interpretation im Ziel-Kontext zu vermeiden**
331 |
332 | Vor der Erläuterung dieser Methodik und deren Abgrenzung gegen Sanitization, möchte ich einige Beispiele für Input im Luca-System und die entsprechenden Output-Kontexte darstellen.
333 |
334 | Geht man davon aus, dass die Kernfuntionalität des Luca-Systems die digitale Abbildung von Kontaktlisten (gefüllt mit Gästedaten), welche (auf Anfrage) an ein berechtigtes Gesundheitsamt übermittelt werden sollen, darstellt, erscheint die Untergliederung in Input/Output zunächst einfach:
335 |
336 | 1. Ein Gast stellt seine Kontaktdaten als Input für eine Location bereit (Output Kontext ist ein einzelner Datensatz für eine Gästeliste, z.B. in Form eines Datenbankeintrages für eine Tabelle)
337 | 2. Aus Sicht der Location ist jeder Gästedatensatz ein Tabelleneintrag für die Gästeliste, welcher als solcher validiert werden muss (z.B. Spaltenanzahl, Typ und Länge der Spaltenwerte). Bevor dieser Datensatz in die Datenbank eingetragen wird, muss er im Kontext der Datenbank codiert werden.
338 | 3. Nach Abfrage durch das Gesundheitsamt, wird eine solche Gästeliste zum Input (der validiert werden muss), der Output-Kontext könnte hier beispielsweise HTML zur Darstellung im Browser sein, aber eben auch ein proprietäres Format zur Weiterverarbeitung in nachgeordneten Fachanwendungen.
339 |
340 | In der technischen Umsetzung gestaltet sich diese Untergliederung weitaus komplexer. Dies soll im nächsten Abschnitt (auszugsweise) aufgezeigt werden.
341 |
342 | ## 3.2 Einige Beispiele für Input und zugehörige Output-Kontexte (exemplarischer Auszug, abstrakt)
343 |
344 | 1. Input: Datensatz aus Kontaktdaten von Nutzern (Name, Adresse, Telefonnummer etc)
345 | 1.1 Output Kontext 1: Visuelle Darstellung im Web-Frontend eines Gesundheitsamtes (Healt-Department Frontend)
346 | 1.2 Output Kontext 2: Werte zur Einbindung in Datenbankabfragen im Health-Department Fronetnd (z.B. Abgleich ob die angegebene Telefonnummer bereits eine TAN-Verifizierung ausgelöst hat)
347 | 1.3 Output Kontext 3: Nutzung in Exportformat im Health-Department Frontend (z.B. Konvertierung zu CSV)
348 | 1.4 Output Kontext 4: Nutzung als Input für Schnittstelle aus dem Health-Department Frontend (z.B. externe API)
349 | 2. Input: Checkin Traces mit "additional data"
350 | 2.1 Output Kontext 1: Visuelle Darstellung im Healt-Department Frontend
351 | 2.2 Output Kontext 1: Visuelle Darstellung der "additional data" im Location Frontend (Tischnummer, sonstige Zusatzdaten die für Gäste erhoben worden)
352 | 2.3 Output Kontext 3: Visuelle Darstellung der "additional data" in der App eines Gastgebers für ein "Private Meeting" (Vor- und Nachname des Gastes)
353 | 2.4 Output Kontext 4: Nutzung als Input für Requests gegen die Luca Backend API (z.B. Auschecken des traces, mit `TraceID` des Inputs)
354 |
355 | Diese Darstellung von Input und Output ist noch immer zu abstrakt, um eine korrekte Filterung anzusetzen. Darüber hinaus wurde der Input hier Kontext-frei betrachtet. Der "additional data" Anteil von traces (vergleiche Abschnitte 1.2 und 1.2.1) kann zum Beispiel Input für das "Location Frontend", für das "Health Department Frontend" als auch für die App des Gastgebers eines "Private Meetings" sein.
356 |
357 | Dennoch sollte bereits hier klar sein, dass das Output Encoding, je nach Kontext, schnell komplex werden kann:
358 |
359 | So wäre z.B. im Kontext der Browser-Darstellung Input der als HTML interpretiert werden könnte, so zu codieren oder zu escapen, dass er eben nicht als HTML interpretiert wird (ein String wie `
..` müsste zu `<img>...` werden) und auch Sub-Kontexte (wie eingebettetes JavaScript) wären zu beachten. Wird die Darstellung von externen UI-Frameworks übernommen (Luca verwendet `React`) ist ggf. auch der Kontext von Templatesprachen zu beachten (um z.B. Angriffe durch "Template Injection" zu verhindern).
360 |
361 | Im Kontext von Datenbankabfragen wäre der Output wiederum so zu kodieren, dass keine unerwünschten Abfragen ausgelöst werden. Einen als String in einer Datenbank zu speichernde Location Name kann durchaus `Hipster Bar'; DROP TABLE users; --'` lauten, wenn man ihn vor Speicherung in der Datenbank z.B. mit Base64 kodiert.
362 |
363 | Setzt man eine saubere Input-Validierung an, kann man bereits viele Problemstellungen ausschließen. Idealer Weise nimmt man eine solche Validierung in zwei Schritten vor, als Beispiel soll hier eine Postleitzahl dienen. Zur Validierung könnten hier die beiden folgende Schritte vorgenommen werden (Luca verarbeitet Postleitzahlen als Zeichenketten, nicht als Integer):
364 |
365 | 1. Validierung der Struktur, z.B. für deutsche Postleitzahlen: Die Postleitzahl besteht nur aus Ziffern, sie überschreitet nicht die maximale Länge von 5 Zeichen
366 | 2. Validierung Semantik/Plausibilität (Kontextabhängige Prüfung): Für Locations sind nur Postleitzahl aus dem Zuständigkeitsereich registrierter Gesundheitsämter zulässig, für Postleitzahlen aus Nutzerkontaktdaten sind nur Postleitzahlen aus den vorgesehenen Regionen zulässig
367 |
368 | Entscheidend für die Input-Validierung ist auch, wo diese ansetzt. Unter funktionalen Gesichtspunkten kann die Validierung dort erfolgen, wo bspw. ein Nutzer Daten bereitstellt (z.B. innerhalb der App). Unter dem Aspekt der Sicherheit, muss eine Validierung von Eingabedaten dort erfolgen, wo eine schadhafte Manipulation weitestgehend ausgeschlossen ist.
369 |
370 | Konkret heißt das, für Nutzerkontaktdaten die erst im Web-Frontend des Gesundheitsamtes entschlüsselt werden, dass die Input Validierung in diesem Frontend, unmittelbar nach der Entschlüsselung vorgenommen werden muss. Luca zeigt kaum Ansätze dies zu tun. Der Großteil der verschlüsselten Daten wird Client-seitig (vor Verschlüsselung validiert). Unter dem Sicherheits-Aspekt kann eine solche Validierung vernachlässigt werden, da Angreifer Daten unter Umgehung des Clients einbringen können. Für unverschlüsselte gespeicherte Daten (z.B. Kontaktdaten iner Location), wird hier für die meisten API Endpunkte des Backends zumindest eine Validierung gegen das Datenbankschema angesetzt (korrekte Datentypen, korrekte Größe/Länge der Daten).
371 |
372 | Für verschlüsselte Daten findet kaum Input Validation oder Output Encoding statt, wenn überhaupt kommt irgendwo in der Verarbeitungskette der Daten "Sanitization" zum Einsatz. Demgegenüber stehen zahlreiche Kontextwechsel im Datenfluss, welche - jeder für sich - Angriffsfläche bieten. Für Angreifer mit durchschnittlichen Kenntnissen, wird dieses Problem auch mit einem kurzen Blick in den Quellcode klar. Auf Entwickler-Seite sollte diese Problem ohnehin offensichtlich sein. Selbst wenn "security-by-design" kein Paradigma ist, müssen Mängel in der Input Validation und im Output Encoding im Testing auffallen. Offensichtlich werden entsprechende Tests aber nur äußerst zaghaft etabliert. Warum entsprechende Fehler in den beworbenen Penetration Tests mit **Whitebox Ansatz** nicht auffallen, lässt sich nicht mehr logisch erklären.
373 |
374 | Die nächsten Abschnitte behandeln **einen** Input-Kontext und **einen** Output-Kontext im Zusammenhang der jüngst öffentlich gemachten "CSV Injection" Schwachstelle, um Fehler im Input/Output Filtering aufzuzeigen, welche auch an anderen Stellen zu finden sind, aber nicht bis zur Demonstration eines funktionierenden Angriffes ausgearbeitet wurden.
375 |
376 | Die Betrachtung beschränkt sich zur Vereinfachung daher zunächst auf "Nutzerkontaktdaten im Health-Department Frontend" als Input und auf "CSV Exporte aus dem Health-Department Frontend" als Output.
377 |
378 | ## 3.3 Vorbemerkung: Öffentliche Darstellung der Sicherheitsmechanismen des Luca-Systems vs Realität
379 |
380 | Der nächst folgende Abschnitt soll exemplarisch darstellen, wie das Luca-System mit Eingabe- und Ausgabefilterung umgeht. Als Beispiel dienen die Kontakdaten von Luca-Nutzern, welche als Input ihren Weg in verschiedene Output-Kontexte **direkt im Applikationsanteil des Luca-Systems, welchen die Gesundheitsämter nutzen, finden (das "Health Department Frontend")**.
381 |
382 | Dennoch erlaube ich mir zunächst einige Vorbemerkungen zur Chronologie der Ereignisse um die jüngst demonstrierte Schwachstelle, denn ich halte diese persönlich für entscheidend, bei der Frage: **"Ist der Hersteller des Luca-Systems vertrauenswürdig?"**
383 |
384 | Diese Frage ist nicht technisch-objektive zu beantworten, aber dennoch essenziell, denn: **Das technische Design des Luca-Systems setzt, in weiten Teilen, einen vertrauenswürdigen Hersteller und Betreiber voraus!**
385 |
386 | Als Ausgangspunkt für die Erläuterungen soll zunächst der Versions-Stand des Luca-Systems in der damaligen Version [v1.1.11 vom 25. Mai 2021 (link)](https://gitlab.com/lucaapp/web/-/tree/v1.1.11) dienen. Es handelt sich dabei um die Version, die im Einsatz war als die [CSV Injection Schwachstelle (Video Link)](https://vid.wildeboer.net/videos/watch/8aba8997-6dd0-45b2-9e14-d1eb1f259f3e) demonstriert wurde, über die das "Health Department Frontend" durch manipulierte Kontaktdaten von Luca-Nutzern angegriffen werden konnte.
387 |
388 | Entscheidend bei der Wahl dieser Version: Bereits am 04. Mai 2021 erschien ein ["Zeit" Artikel (link)](https://www.zeit.de/digital/datenschutz/2021-04/luca-app-gesundheitsaemter-corona-kontaktverfolgung-hackerangriff-risiko), in dem das Risiko von "CSV Injections" durch manipulierte Nutzer-Kontaktdaten thematisiert wurde. Der Artikel hatte eine technische Tiefe, bei der sogar von einzelnen Zeichen gesprochen wurde, die einen solchen Angriff auslösen können:
389 |
390 | ```
391 | ... Das ist der Grund, warum CSV-Dateien beispielsweise keine Sonderzeichen wie "=", "@" oder ";" enthalten sollten und vor dem Import validiert oder gefiltert werden müssen, damit sie beim Einlesen keinen Schaden anrichten ...
392 | ```
393 |
394 | Der CEO des Luca-Herstellers kam in dem selben Artikel zu Wort und stellte klar:
395 |
396 | ```
397 | "Eine Code Injection über den Namen ist bei Luca im Gesundheitsamt nicht möglich." Sollte im Namen Schadcode enthalten sein, werde dieser von React – der dahinterliegenden Software-Bibliothek, die Luca nutze – entsprechend behandelt und "sichergestellt, dass hier kein Schaden entsteht". Auch in Sormas selbst werde das Thema "sicherlich auch nach etablierten Standards umgesetzt".
398 | ```
399 |
400 | In einer öffentlichen Diskussion zum Thema, knokretisierte der CEO sein Statement via [Twitter(link)](https://twitter.com/patrick_hennig/status/1389613832742612994) noch am Tag des Artikels und legt dar, dass bezüglich CSV Injections "... beim Entschlüsseln der Daten die OWASP Empfehlungen ..." zu CSV Injections umgesetzt werden:
401 |
402 | ```
403 | Das war aber nur ein Teil meiner Antwort. Der zweite Teil, dass sowohl SORMAS beim Import als auch wir beim Entschlüsseln der Daten die OWASP Empfehlungen zu CSV Injections umsetzen, steht leider nicht da.
404 | ```
405 |
406 | Dieser Austausch entstand, aufgrund meiner kritischen Äußerungen gegenüber der Argumentation, dass man versucht Code Injections mittels "ReactJS" zu verhindern (React wäre für Output-Kontexte wie JavaScript oder HTML relevant - zur Verhinderung von Cross-Site-Scripting, aber nicht für Code Injections in Kontexten wie CSV). Aus dem Twitter-Statement war außerdem ableitbar, dass die zitierten OWASP Empfehlungen "beim Entschlüsseln der Daten" angelegt werden (also an den Input), nicht etwa an die resultierenden CSV-Daten (also Output Encoding/Escaping). Dies erschien **nicht schlüssig**. Sollte die Filterung tatsächlich direkt an Eingabedaten vorgenommen werden, würde dies zu technischen Problemen führen (Missachtung andere Output-Kontexte, Encoding für den CSV-Kontext würde sich beispielsweise auf den Output-Kontext für Web-basierte Browserdarstellung auswirken usw.).
407 |
408 | Bis zum erscheinen des "Zeit" Artikels vom 04. Mai 2021 war ich zwar mit verschiedenen Aspekten des Luca-Systems befasst, aber nicht mit dem konkreten Szenario von "CSV Injections". Nur einen Tag später, am 05. Mai 2021, wurde ich erneut via Twitter auf das Problem aufmerksam.
409 | Ein Tweet unter dem handle "@gnirbel" stellte fest, dass die beworbenen OWASP Mitigationsmaßnahmen gegen "CSV Injection" erst wenige Stunden vor Veröffentlichung des "Zeit" Artikels - am 03. Mai 2021 - in den Quellcode gepatcht wurden ([Link zum Tweet](https://twitter.com/gnirbel/status/1389949006000934915)).
410 |
411 | Hier wurde nicht nur erneut so schnell am Live-System gepatcht, dass Tests offensichtlich gar nicht durchfühbar waren. Dabei ging hier um ein Sicherheitsfeature, welches wenige Stunden nach dem Patch als umgesetzt beworben wurde (Beachtet man die Vorlaufzeit zur Veröffentlichung eines solchen Artikels, muss man sogar davon ausgehen, dass der Patch eingepflegt wurde, nachdem gegenüber der "Zeit" erklärt wurde, dass OWASP Vorgaben zur Mitigation von CSV Injections umgesetzt waren).
412 |
413 | Aufgrund dieses Tweets wurde auch sehr schnell klar - was vorher unklar war:
414 |
415 | Setzt das Output-Encoding für den CSV-Kontext wirklich direkt "beim Entschlüsseln der Daten" (am Input) an?
416 |
417 | Der "Filter" aus besagtem Patch befand sich hier: [Code link](https://gitlab.com/lucaapp/web/-/commit/2f878ef9e624224722aa073ee71cb8703f6728f1?page=7#7691cd5a586200d7401cc54324010eae3f559fdc)
418 |
419 | Angwendet wurde er [hier](https://gitlab.com/lucaapp/web/-/commit/2f878ef9e624224722aa073ee71cb8703f6728f1?page=7#18895b77c1e5c724c891e09f24ab44236d617e56_20_51) und angewendet wurde er [hier](https://gitlab.com/lucaapp/web/-/blob/2f878ef9e624224722aa073ee71cb8703f6728f1/services/health-department/src/utils/decryption.js#L90) und [hier](https://gitlab.com/lucaapp/web/-/blob/2f878ef9e624224722aa073ee71cb8703f6728f1/services/health-department/src/utils/decryption.js#L162).
420 |
421 | Ein sehr kurzer Blick in diesen "schnell nachgeschobenen" Patch genügt, um festzustellen:
422 |
423 | - Filterung wird tatsächlich unmittelbar nach der Entschlüsselung von Rohdaten angesetzt (nach einem Kontext-Wechsel zu JavaScript, denn die entschlüsselten Daten wurden "noch schnell" als JSON geparst)
424 | - Auch ohne umfassende Kenntnis des Luca-Codes wird damit klar: **Eine Filterung an dieser Stelle kann sich nicht auf einen CSV Output-Kontext beziehen, denn es gibt hier noch keinen CSV-Output**
425 | - Es handelt sich auch nicht um eine Input Validierung, denn die Funktionalität validiert kaum Annahmen, die über den Input getroffen werden müssen
426 | - statt Eingabe-Validierung und Ausgabe-Kodierung kommt eine Art "Sanitization" zum Einsatz, die offensichtlich an einer Stelle platziert wurde, an denen nicht allen relevanten Ausgabe-Kontexten Rechnung getragen werden kann, welche aus Sicherheitsgründen gefiltert werden müssten (man könnte hier seitens Entwickler allerdings entgegen halten, dass man "maliziösen Input" ausschließlich für den Sonderfall "CSV Injection" behandeln wollte)
427 |
428 | Nach dieser (sehr ausführlichen) Einleitung soll nun zunächst betrachtet werden, welche Kontextwechsel der User-Input im "Health Department Frontend" durchläuft (beschränkt auf verschlüsselte Kontaktdaten)
429 |
430 | ## 3.4 Verarbeitungskette von Nutzer-Input zu resultierenden Output-Kontexten im "Health Department Frontend" (Rückblick auf v1.1.11)
431 |
432 | Betrachtet werden hier (stellvertretend) nur die Kontaktdaten der Luca-Nutzer als Input, es sei aber nochmals darauf hingewiesen, dass es verschiedene weitere Eingabedaten gibt, die der Kontrolle von externen Teilnehmern unterliegen.
433 |
434 | Die Kontaktdaten sind ab dem Moment, in dem sie im Luca-Applikationsanteil der Gesundheitsämter verarbeitet werden (im "Health Department Frontend"), als **unvalidierter Input** zu sehen. Eine vorherige, Client-seitige Validierung der Kontaktdaten hat keinen relevanten Effekt, da diese immer durch den Nutzer umgangen werden kann. Vielmehr, laufen die besagten Kontaktdaten verschlüsselt im Gesundheitsamt aus, so dass auch eine Vorab-Validierung durch das Luca-Backend ausgeschlossen ist. Die Validierung der Daten und die Kontext-agnostische Kodierung kann deshalb erst im Gesundheitsamt erfolgen.
435 |
436 | Funtionale Prozesse, die den Abruf, die Entschlüsselung und weitere Verarbeitung der Daten auslösen, werden nicht mitbetrachtet.
437 |
438 | ### Schritt 1: Entschlüsselung der Kontaktdaten
439 |
440 | Zunächst werden die bereits auf dem Server hinterlegten `encrypted contact data` des Nutzers abgerufen: [Code Link](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/health-department/src/utils/cryptoOperations.js#L70).
441 |
442 | Die verschlüsselten Daten liegen zunäschst als Base64 codierter String mit einer maximalen Länge von 1024 Byte vor ([Code Link](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/backend/src/database/models/users.js#L15)) und werden zunächst dekodiert ([Code Link](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/health-department/src/utils/cryptoOperations.js#L80)). Ein ungültiger Base64 String würde hier eine Exception auslösen, ob diese zu einem "crash" des "Health Department Frontend" oder zum "Übergehen" des Datensatzes führt, wurde nicht geprüft. Nach der Base64-Dekodierung ergeben sich bis zu 768 Bytes an Binär-Daten, die durch externe Nutzer **beliebig gestaltet werden können** (werden als Hex-kodierter String repräsentiert).
443 |
444 | Nach der Entschlüsselung der Kontaktdaten (AES128 CTR), wird der Klartext als UTF-8 String dekodiert ([Code Link](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/health-department/src/utils/cryptoOperations.js#L77)). Ergäbe sich hier kein gültiges UTF-8, würde dies erneut eine Exception auslösen.
445 |
446 | Es kann also festgehalten werden, dass an diesem Punkt ein UTF-8 String, mit einer Länge bis zu 768 Bytes vorliegt, welcher durch Systemnutzer beliebig gestaltet werden kann.
447 |
448 | ### Schritt 2a: Überführen der Kontakten in ein JavaScript Objekt (Kontext Wechsel)
449 |
450 | Der UTF-8 String aus Schritt 1 durchläuft nun den ersten Kontext-Wechsel und wird mittels `JSON.parse()` in ein JavaScript Objekt überführt ([Code Link](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/health-department/src/utils/cryptoOperations.js#L87)). Die Input Validation entfällt hier auf den JSON Parser, ein ungültiger JSON String würde ebenfalls eine Exception auslösen.
451 |
452 | An dieser Stelle ist festzuhalten, dass das **erwartete** Objekt folgendes Schema haben sollte (Pseudocode):
453 |
454 | ```
455 | {
456 | "v": Typ Integer/Number // 3 für App-Nutzer, 2 für Badges
457 | "fn": Typ String, Länge beliebig // Vorname
458 | "ln": Typ String, Länge beliebig // Nachname
459 | "pn": Typ String, Länge beliebig // Telefonnummer
460 | "e": Typ String, Länge beliebig // Email
461 | "st": Typ String, Länge beliebig // Strasse
462 | "hn": Typ String, Länge beliebig // Hausnummer
463 | "pc": Typ String, Länge beliebig // Postleitzahl
464 | "c": Typ String, Länge beliebig // Stadt
465 | "vs": Typ String, Länge beliebig // nur für Badges, userVerificationSecret (Base64 kodiert)
466 | }
467 | ```
468 |
469 | Als Input Validation erfolgt durch die Funktion `assertStringOrNumericValues` eine Typen-Überprpfung des Kontaktdaten-Objektes ([Code Link](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/health-department/src/utils/cryptoOperations.js#L88)). Die Funktion löst eine Exception aus, wenn eine der `properties` des Objektes einen Wert enthält der nicht dem Typ `number` oder `string` entspricht ([Code Link](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/health-department/src/utils/typeAssertions.js#L3)).
470 |
471 | **Diese rudimentäre Eingabe Validierung ist unzureichend**:
472 |
473 | - Es findet keine vollständige Validierung der Struktur statt. Ein Vorname darf hier beispielsweise durchaus vom Typ `number` sein oder die Objekt-Property `"v"` wird mit einem Wert des Typs `string` belegt. Dies kann nicht vorhersehbare Folgen auf den Programmfluss der Applikation im Gesundheitsamt haben, wie z.B. einen "Crash" beim Verarbeiten eines manipulierten Datensatzes.
474 | - Da hier (noch) nicht geprüft wird, ob ausschließlich Objekt-Properties vorhanden sind, welche genäß der erwarteten Struktur vorgesehen sind, kann der Weg für Angriffe im JavaScript-Kontext geebnet werden. Als Beispiel sei hier die Vobereitung einer "Prototype Pollution" genannt (Objekt-Schlüssel wie `__proto__` oder `constructor` werden nicht unterbunden). Eine diesbezügliche Eingabe Validierung erfolgt erst später.
475 | - Es findet keinerlei semantische Validierung statt. Eine Postleitzahl kann beispielsweise eine Zeichenkette beliebiger Länge, ohne weitere Beschränkung der zulässigen Zeichen, darstellen, welche (je nach Output Kontext) vollkommen anders interpretiert werden kann.
476 |
477 | **Das diese Unter-Sektion "Schritt 2a" und nicht "Schritt 2" heißt, ist der überwiegend schlechten Code Qualität des Systems geschuldet. Zur Erläuterung ist nun doch ein Exkurs in die Prozess-Gestaltung des Luca-Systemes nötig. Der hier beschriebene Ablauf trifft nur für Kontaktdaten zu, die durch das Gesundheitsamt direkt bei NutzerInnen abgerufen werden, im Rahmen der Abfrage ihrer Location Historie (Infektionsfall, vergleiche hierzu [Luca Security Overview "Tracing the Check-In History of an Infected Guest"](https://luca-app.de/securityoverview/processes/tracing_access_to_history.html)). Dass ein Nutzer der direkt vom Gesundheitsamt kontaktiert wird, versucht einen Angriff mit manipulierten Kontaktdaten durchzuführen, ist ohnehin eher unwahrscheinlich.**
478 |
479 | **Es gibt allerdings weitere Prozesse, in denen manipulierbare Kontaktdaten als Input verarbeitet werden, nämlich dann wenn das Gesundheitsamt die Kontakte einer Infizierten Person bei den Locations abfragt, welche von dieser Person besucht wurden (Gästelisten). Der funtionale Prozess wird hier beschrieben: [Luca Security Overview "Finding Potential Contact Persons"](https://luca-app.de/securityoverview/processes/tracing_find_contacts.html). Wesentlich hierbei: Ein Angriff auf diesen Prozess ist viel wahrscheinlicher, da Angreifer beliebig viele Nutzer mit Schadhaften Kontaktdaten anlegen und in belieibig viele Locations einchecken können (vergleiche Abschnitt 2.3 und 2.7). Die Datenverarbeitung im Gesundheitsamt erfolgt, sobald eine Location die "Gästelisten" bereitstellt (die Kontaktdaten sind dabei durch LocationbetreiberInnen nicht einsehbar/validierbar).**
480 |
481 | **Was heißt "schlechte Code Qualität"? Ganz einfach: Der Code zur Entschlüsselung der Kontakdaten ist redundant vorhanden, nimmt aber an anderen Stellen keine Filterung mehr vor. Dies ist nur ein Beispiel für eine durchgängig unsaubere, überkomplizierte, Wartungs-intensive und Fehler-anfällige Code-Struktur. Für durchschnittliche Entwickler dürfte die Fehleranalyse und -beseitigung am statischen Code extrem aufwendig sein. Man müsste hier schon mindestens mit dynamischer Analyse ansetzen und dabei riskieren Angriffs-relevante Code-Pfade nicht zu erkennen. Automatisierte und manuelle Tests werden zwar beworben, aber unter dem Stichwort "Code Coverage" im Hinterkopf, wird man diesbezüglich kaum fündig werden. "Threat Actors" dürfen sich hier freuen, denn diese werden Angriffs-Vektoren eher über "Fuzzing", automatisierte "Control-flow Analysis" oder "Instrumentation" ausfindig machen, und dabei sicher schneller fündig als der Code angepasst werden kann.**
482 |
483 | **Aus "Schritt 2a" wird also im nächsten Abschnitt der eigentliche "Schritt 2".**
484 |
485 | #### Randbemerkung zur JSON Input Validierung:
486 |
487 | _An anderen Stellen des Luca-Systems wird ähnlich verfahren (keine vollständige Eingabe Validierung), allerdings kommen andere JSON-Parser zum Einsatz. So nutzt die Android App zum Beispiel `Gson` als Parser, welcher für Gleitkomma-Typen auch Werte wie `NaN` (Not-a-Number)oder `INF` (Infinity) zulässt. Insbesondere eine ungefilterte Nutzung eines des Wertes `NaN` im Java-Kontext der Android App, würde dazu führen, dass die Ergebnisse aller in der Verarbeitung vorkommenden Operationen ebenfalls `NaN` werden (Stichwort "NaN Poisoning")._
488 |
489 | ### Schritt 2: Überführen aller Kontakten einer "Gästeliste" in JavaScript Objekte (Kontext Wechsel)
490 |
491 | Wie erläutert, existiert die bereits beschriebene Funktionalität zum entschlüsseln von Kontaktdaten auch an anderen Stellen im Code:
492 |
493 | - für Kontaktdaten von Location-Gästen mit App: [Code Link](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/health-department/src/utils/decryption.js#L124)
494 | - für Kontaktdaten von Location-Gästen mit Schlüsselanhängern: [Code Link](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/health-department/src/utils/decryption.js#L58)
495 |
496 | **In keinem der beiden Code-Pfade findet die in "Schritt 2a" beschriebene (rudimentäre) Validierung der JavaScript Objekte die sich aus beliebig wählbaren Kontakdaten ergeben noch statt.**
497 |
498 | Die beiden vorgenannten Code-Pfade werden in der Funktion `decryptTrace` zusammengefasst, welche neben anderen Daten (nicht im Scope dieses Dokumentes), auch das JavaScript Objekt - welches aus den entschlüsselten Kontaktdaten entsteht - als `userData` zurück gibt ([Code Link](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/health-department/src/utils/cryptoOperations.js#L92)).
499 |
500 | An dieser Stelle kann man festhalten, dass das resultierende `userData` Objekt beliebige Properties, Werten beliebiger Typen abbilden kann, die durch Luca-Nutzer frei wählbar sind.
501 |
502 | Die beinhaltet auch "nested objects", die z.B. genutzt werden können, um bei einem weiteren Kontext-Wechsel zu Frameworks wie `React` eine "Client-side Template Injection" zu provozieren, wenn die Daten als unvalidierter Input für eine unsichere `React Component` dienen (diesbezüglich wurden keine Betrachtungen unternommen). Als Nachlese sei hier auf folgenden Artikel verwiesen: [Link](https://medium.com/dailyjs/exploiting-script-injection-flaws-in-reactjs-883fb1fe36c1). Eine Objekt wie das im Artikel erwähnte, wäre als `userData` problemlos platzierbar:
503 |
504 | ```
505 | // result in userData, after decryption and JSON parsing
506 | {
507 | _isReactElement: true,
508 | _store: {},
509 | type: "body",
510 | props: {
511 | dangerouslySetInnerHTML: {
512 | __html:
513 | "
Arbitrary HTML
514 |
515 | link"
516 | }
517 | }
518 | }
519 | ```
520 |
521 | #### Randbemerkung
522 |
523 | _Im Kontext des Angriffsvektors "Prototype Pollution" genügt dies allein nicht, da `JSON.parse()` Objekt-Schlüssel wie `__proto__` als reine Property bedient (statt einen `Setter` zu verwenden). Die relevanten Properties, müssten in der weiteren Verarbeitung noch unter Nutzung eines `Setters` in einem JavaScript-Objekt platziert werden._
524 | **Bemerkenswert ist allerdings, dass der Backend Code (auch in der aktuellsten Version, `v1.2.1` at time of this writing) Nutzer-Input in einer Form verarbeitet, die hierzu als "door opener" dienen kann.** [Link zu Code Beispiel: Durch Location Betreiber frei wählbare Namen für "addititional data" Felder.](https://gitlab.com/lucaapp/web/-/blob/v1.2.1/services/contact-form/src/components/hooks/useRegister.js#L30) _Hier hier wären z.B. Input wie `additionalData-constructor` als Object-key möglich._
525 |
526 | ### Schritt 3: Abruf der Daten als state von `React` Komponenten (erneuter Kontext-Wechsel)
527 |
528 | Für "Schritt 2" wurde festgehalten, dass unvaldierte Nutzerdaten durch die Funktion [decryptTrace (link)](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/health-department/src/utils/cryptoOperations.js#L92) als JavaScript Objekte bereitgestellt werden (nach mehreren Kontextwechseln). Abgesehen vom hoffentlich vorhandenen Exception-Handling beim Dekodieren der Daten, findet hierbei so gut wie keine Filterung statt. **Es sei daran erinnert, dass die Eingabe-Validierung unterlassen wird, obwohl sehr genaue Informationen über die erwartete Struktur und Semantik dieser Daten vorliegen.**
529 |
530 | Diese Daten werden aber nicht einfach in die jeweiligen Ausgabe-Kontexte "gepusht", sondern die Funktion `decryptTrace` muss aufgerufen werden ("pull" Prinzip). Da es sich beim "Health Department Frontend" um eine JavaScript-basierte Web-Applikation handelt, deren Logik weitestgehend "lokal" (im Browser des Gesundheitsamtes) läuft, könnte man annehmen, dass die Daten zur Verarbeitung keinen weiteren Kontext-Wechsel durchlaufen, bevor sie den vorgesehenen Ausgabe-Kontexten zugeführt werden. Die Implementierung spricht hier allerdings eine andere Sprache.
531 |
532 | Die besagte `decryptTrace` Funktion wird [hier (Code Link)](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.react.js#L57) aufgerufen. Sie wird auf ein ganzes Array von Kontaktdaten angewendet (die Daten die der "Gästeliste" einer Location zu einem definierten Zeitpunkt entsprechen). Weniger entscheidend ist dabei die Funktion die unmittelbar im Anschluss invalide Datensätze herausfiltern soll ([Code Link](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.react.js#L61)), denn hier wird lediglich geprüft, ob die "Check-ins" zu denen die Kontaktdaten entschlüsselt wurden, über eine gültige Signatur verfügt haben. (Dies kann eine AngreiferIn ohne Probleme sicherstellen. Auch können schadhafte Kontakdaten problemlos mit gültigen Signaturen versehen werden).
533 |
534 | Entscheidend ist, wo dieser Code-Anteil ausgeführt wird. Denn, die `ecryptTrace` Funktion wird in einer anonymen JavaScript Funktion ausgeführt, welche als Argument an eine `useEffect` Funktion übergeben wird ([Code Link](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.react.js#L44)).
535 |
536 | Die `useEffect` Funktion ist allerdings nicht mehr Bestandteil des Applikationscodes des Luca-Systems, sondern gehört zu dem bereits erwähnten `React` Framework. Es handelt sich dabei um eine (gängige) Drittanbieter-Library, deren Kernzweck die Erstellung von JavaScript-basierten Benutzerinterfaces ist ("Web UI"). `React` ist weder keine Komponente zur sicheren Verwaltung, Verarbeitung oder Modellierung von Daten. Sie geht sehr wohl mit Daten um ("state" des User Intefaces) und platziert auch einige Sicherheitsmaßnahmen - aber eben nur für den Ausgabe-Kontext eine Web-basierten User Interfaces (z.B. Mitigation von Cross-Site-Scripting beim Darstellen von Nutzerkontrollierten Inhalten). `React` behandelt explizit nicht andere Ausgabekontexte. Die `useEffect` Funktion, bildet beispielsweise einen sogenannten "hook" ab, die den **"state" des User Interfaces** aktualisiert, wenn sich Daten ändern, welche in einer `React` Komponente dargestellt werden (vergleiche [React Dokumentation (Link)](https://reactjs.org/docs/hooks-effect.html)). Im Konkreten Fall heißt das, die Daten werden wie beschrieben verarbeitet, sobald im User Interface des Gesundheitsamtes, die Darstellung für "Gästelisten (Kontaktlisten)" einer Person geladen werden.
537 |
538 | Dies mag zunächst sinnvoll erscheinen, wenn denn dise Darstellung der einzige Ausgabe-Kontext wäre. In einem solchen Fall könnte man es sogar riskieren, das Output-Encoding für den Kontext der Web-Basierten Darstellung zu unterlassen, und sich allein auf die Schutzmaßnahmen des `React` Frameworks zu verlassen (und dabei hoffen, dass nie ein anderes UI Framework verwendet werden muss). **Allerdings handelt es sich nicht um den einzigen Ausgabe-Kontext! Die unvalidierten Kontaktdaten Objekte, welche nun zentral im "React state" hinterlegt sind, sind "Single Source of Truth" für weitere Ausgabe-Kontexte. Neben Export-Formaten wie SORMAS, Excel und CSV, dient dieser "User Interface State" auch als Input für Anfragen and die SORMAS REST API.**
539 |
540 | Der "data flow" innerhalb des React-Kontextes wird noch konfuser:
541 |
542 | Der `useEffekt` hook, welcher die Daten verarbeitet, ist Bestandteil der "React Komponente" `ContactPersonViewRaw` ([Code Link](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.react.js#L22)), welche wiederum in eine `React.memo` Komponente mit dem Namen `ContactPersonView` eingebettet wird ([Code Link](https://gitlab.com/lucaapp/web/-/blob/76c4978425133286a6a72f02643e0c2207d04f46/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.react.js#L108)). Liest sich kompliziert? Ist es auch! Von sauberer, nachvollziehbarer und gut dokumentierter Code-Struktur kann hier nicht mehr die Rede sein. Mit Blick auf die Daten, die hier verarbeitet werden, und das - mangels Filterung - bestehende Risikopotential, erscheint der Code nicht nur unprofessionell, sondern gefährlich. Das Stichwort "Client Side Template Injection", welches im `React Kontext` schlagartig an Relevanz gewinnt, wurde schon genannt. Man kann nur hoffen, dass alle React Komponenten, die diese Daten verwenden, entsprechend getestet und abgesichert sind.
543 |
544 | Zur Erinnerung: Bisher gab es **für keinen Kontextwechsel, den die (beliebig gestaltbaren) Nutzerdaten durchliefen, eine ausreichende Input Validierung**. Es gab sie weder syntaktisch, noch strukturell, noch semantisch!
545 |
546 | Der behandelte Code gehört, wie ausgeführt, zu einer `React` Komponente. Dies wird auch am Dateinamen `ContactPersonView.react.js` ersichtlich ([Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.react.js)). Nochmals unterstreichen möchte ich dies, um den Kontext-Wechsel herauszustellen. Es findet hier nicht nur ein funktionaler Kontextwechsel der Daten statt (in das User Interface), sondern auch ein syntaktischer Kontextwechsel: `React` bietet als Template-Library Sprachfunktionen, die über die Syntax von HTML oder JavaScript hinaus gehen - eine sogenannte Templatesprache ([ersichtlich am verlinkten Code-Abschnitt, der weder gültiges HTML noch JavaScript darstellen würde](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.react.js#L86)).
547 |
548 | Auch in diesem Kontext von React-Templates existieren Sprach-Konstrukte, die bei fehlender Input Validierung oder ungenügendem Output Encoding als Angriffs-Verktoren nutzbar gemacht werden können ("Template Injection" wurde hier bereits als Beispiel genannt).
549 |
550 | Als wäre der Weg der Daten bis zu den Ausgabe-Kontexten nicht schon verworren genug, ist man hier offensichtlich bemüht die Dinge noch komplizierter, **und damit noch Fehler-anfälliger**, zu machen. Ich beschreibe dies nur noch in Stichpunkten:
551 |
552 | - Die React Komponente `Header.react.js` übernimmt das `traces` Argument ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/Header/Header.react.js#L19))
553 | - Die Komponente wird unter dem Namen `Header` exportiert ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/Header/Header.react.js#L83)) und in die (bereits vorgestellte) Komponente [ContactPersonView.react.js (Code Link)](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.react.js#L92) eingebettet. Die `Header` Komponente übernimmt dabei das als Argument `traces` ein Array namens `selectedTraces`, welches **für jeden Eintrag** eines der `userData` Objekte enthält (welche nicht validiert Eingabedaten enthalten).
554 | Erläuterungen über den Weg der [hier)](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.react.js#L57) erstellten `traces` (jeweils mit ungefilterten `userData` Objekten), zum `selectedTraces` Array welches [hier (Code Link)](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.react.js#L92) verwendet wird, erspare ich mir.
555 | Grund: Die "Filterung" hat nur funktionalen Charakter (Ausschluss von Kontaktdaten-Einträgen aus der Gästeliste, die eine ausgewählte minimal-Kontaktzeit unterschreiten. "Per default" werden hier zunächst alle Kontakte gelistet).
556 | - Die React Komponente `Header.react.js` bettet weitere React Komponenten ein, welche verschiedene Ausgabe-Konexte bedienen sollen. **Diese Sub-Komponenten haben keine Relevanz in der Darstellung des User Interfaces!** Der relevante Anteil für die (dynamische) Darstellung im User Interface, ist hier lediglich ein Download-Button ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/Header/Header.react.js#L76))
557 | - Das eigentliche Data-Processing(und das **je Ausgabe-Kontext** benötigte Output-Encoding, wird auf weitere (zweckentfremdete) User Interface Komponenten deligiert, an die das `traces` Array mit `userData` Objekten weitergereicht wird, im einzelnen:
558 | - an eine React Komponente für den CSV Export ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/Header/Header.react.js#L50)), welche hier implementiert wird: [CSVDownload (Code Link)](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.helper.js#L313)
559 | - an eine React Komponente für den Excel Export ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/Header/Header.react.js#L53)), welche hier implementiert wird: [ExcelDownload (Code Link)](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.helper.js#L84)
560 | - an eine React Komponente für den **CSV-basierten SORMAS Export** ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/Header/Header.react.js#L56)), welche hier implementiert wird: [SormasDownload (Code Link)](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.helper.js#L508)
561 | - (wenn aktiviert) an eine React Komponente für den Export über **die SORMAS Rest API** ([Code Link 1](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/Header/Header.react.js#L60), [Code Link 2](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/Header/Header.react.js#L38)), welche hier implementiert wird: [SormasModal (Code Link)](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/SormasModal/SormasModal.react.js).
562 | - die `SormasModal` Komponente erweitert jeden Eintrag `traces` Array um eine `uuid` property und speichert das neue Array in `newTraces`. Das ungefilterte `userData` Objekt wird dabei für jeden Eintrag (mit allen weiteren Properties eines `trace`) übernommen ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/SormasModal/SormasModal.react.js#L49)).
563 | - Unmittelbar im Anschluss wird das `newTraces` array (mit den ungefilterten `userData` Objekten) an den `sormasClient.personPush()` Funktion weitergegeben ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/SormasModal/SormasModal.react.js#L52)). Die Funktion ist hier implementiert und nimmt für die Properties der `userData` **noch immer keine Input Validierung vor**: [Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/network/sormas.js#L35)
564 |
565 | ## 3.5 Zwischenfazit
566 |
567 | Bisher wurde ausschließlich die `v1.1.11` des Luca "Health Department Frontends" betrachtet. Die Betrachtung dieser Version wurde auf einen sehr konkreten Fokus reduziert, nämlich die Frage:
568 |
569 | **Sind Gesundheitsämter durch den Angriffsvektor "CSV Injection" gefährdet?**
570 |
571 | Auslöser der Betrachtung war ein eilig nachgereichter Patch, der in diese Luca-Version - **im engen zeitlichen Zusammenhang zu einer Presse anfrage zum Thema "CSV Injections"** - einfloss. Im gleichen Zuge wurde durch die Luca-Entwickler kommuniziert, dass das System entlang der OWASP Emfehlungen gegen diesen Angriffs-Vektor abgesichert sei.
572 |
573 | Weiter wurde die Betrachtung von Nutzer-kontrollierten Eingabedaten, welche im Luca-System verarbeitet werden, auf "Kontaktdaten" reduziert.
574 |
575 | Trotz dieses engen Betrachtungsfokus, zeigen sich schnell eklatante Mängel im grundsätzlichen Umgang mit solchen unvalidierten Daten. Insbesondere wurde aufgezeigt, dass die Daten verschiedene Sprach-Kontexte durchlaufen, ohne jemals validiert oder gar konform zum Zielkontext kodiert werden. Ich möchte dies hier aus folgenden Gründen festhalten:
576 |
577 | - Code Injection ist in vielen dieser Kontexte möglich, aber Mitigation beschränkt sich hier offensichtlich auf den Vektor "CSV Injection" (auch wenn disbezügliche Maßnahmen bisher noch nicht ersichtlich waren). Für den Kontext der SORMAS Rest API findet zum Beispiel keinerlei Input- oder Output-Filterung statt.
578 | - Eingabe Validierungen an den richtigen Stellen, könnten bereits zahlreiche Injection-Vektoren mitigieren (Code Injections die auf den Datenbank-Kontext abzielen, JavaScript Injection/Cross-Site-Scripting, Template Injection, Prototype Pollution ... um nur einige zu nennen). Dies wäre allein deshalb möglich, weil für die Kontaktdaten bereits nach Entschlüsselung eine klar definierte Struktur erwartet wird. Diese wird aber nicht erzwungen. Ebensowenig werden semantische Regeln an den Input angelegt, die allein viele Zeichen ausschließen könnten, welche für Code Injection in den verschiedenen Kontexten nötig wären.
579 | - **Die Daten-Handhabung in der Implementierung bewegt sich abseits aller "best practices". Sie ist übermäßig kompliziert, sie ist nicht konsistent (redundante Funktionalität mit unterschiedlicher Implementierung), sie erscheint hochgradig fehleranfällig. Code wie der vorliegende ist äußerst schwer zu testen, nicht nur unter dem Sicherheitsaspekt, auch funktional! (Das in Abschnitt 2.8.2 dargestellte Problem zu Schlüsselanhängern, die durch Gesundheitsämter nicht auswertbar waren, unterstreicht diese Annahme)**
580 | - Die Diskrepanz zwischen "mangelnder Sorgfalt" und "Sensitivität der von Luca verarbeiteten Daten" lässt nur zwei Schlüsse zu: Es fehlt den Entwicklern an Erfahrung oder es wird vorsätzlich ein Sicherheitsniveau beworben, von dem man weiß, dass es nicht gehalten werden kann.
581 | - Ein Blick in den Code von System-Komponenten, welche außerhalb des hier gewählten Betrachtungsfokus liegen, zeigt keine gesteigerte Qualität. Man muss daher annehmen, dass das gesamte System mit Implementierungsmängeln behaftet ist, welche unmittelbar in Sicherheitsrisiken münden.
582 | - Statt den sicheren Umgang mit Daten von hochgradig-sensitivem Character in gut definierten, sauber ausgestalten und nachvollziehbar implementierten Prozessen abzubilden, vertraut man diese Daten externen Drittanbieter-Bibliotheken an (Beispiele folgen). Hierbei wird die Kontrolle über die **robuste** Datenverarbeitung nicht nur vollständig abgegeben, es sind darüberhinaus keinerlei Tests erkennbar, die sicherstellen könnten, dass die ausgelagerten Funktionalitäten auch die erwarteten Ergebnisse liefern. Dies wiegt umso schwerer, wenn komplexe Drittanbieter-Libraries eingebunden werden, um am Ende nur rudimentäre Teilfunktionalitäten zu nutzen, welche sich innerhalb des Luca-Codes mit geringen Aufwand kontrolliert abbilden liesen. Das fehlende Verständnis für das Paradigma "Security-By-Design" wird hier überdeutlich.
583 |
584 | ## 3.6 Zusammenfassung: Ungefilterter Input (`userData`), auch in der aktuellesten Luca-Version `v1.2.1`
585 |
586 | Am Ende des Abschnitts 3.4 wurden einige Output-Kontexte für ungefilterte Kontaktdaten angesprochen.
587 |
588 | Dieser Abschnitt soll einige dieser Output Kontexte betrachten, denn hier müsste der Input passend zum jeweiligen Kontext kodiert werden. Der Input sollte dabei bereits validiert sein. Zunächst soll resümiert werden, von welchem Input hier die Rede ist:
589 |
590 | - Der Input wird als JavaScript Array namens `traces` bereitgestellt. Es entspricht der "Gästeliste" einer Location (**alle** Luca-Nutzer die überschneiden mit der Person die seitens Gesundheitsamt "getracet" anwesend waren)
591 | - Jeder Eintrag (`trace`) in diesem Array, ist ein JavaScript Object welches weitere eingebettete Objekte mit den "Check-In-Daten" weitere Luca-Nutzer enthält
592 | - Eines der eingebetteten Objekt zu jedem trace ist das (bereits umfassend beschriebene) `userData` Objekt, welches aufgrund der fehlenden Eingabevalidierung durch beliebig gestaltet werden kann
593 |
594 | Eine Pseudo-Code Darstellung des `traces` Array:
595 |
596 | ```
597 | // traces array (handled as "React" Component state):
598 | [
599 | {
600 | "traceID": ...,
601 | "checkin": ...,
602 | "checkout": ...,
603 | // arbitraryUserData:
604 | // base64 string with length up to 1024 characters, no content restriction, if:
605 | // - proper base64
606 | // - properbly encrypted (up to 768 arbitrary bytes)
607 | // - if decryption results in proper JSON string (UTF-8 encoded)
608 | "userData": JSON.parse(AES128_CTR_decrypt(Base64_decode(arbitraryUserData))),
609 | "additionalData": ...,
610 | "isInvalid": ...
611 | },
612 | {
613 | ...
614 | "userData": JSON.parse(AES128_CTR_decrypt(Base64_decode(arbitraryUserData2))),
615 | ...
616 | },
617 | {
618 | ...
619 | "userData": JSON.parse(AES128_CTR_decrypt(Base64_decode(arbitraryUserData3))),
620 | ...
621 | },
622 | ...
623 | ]
624 | ```
625 |
626 | Erwähnt sei, dass die `additionalData` Objekte ebenfalls ohne weitere Filterung "auflaufen" und (aufgrund ihrer Handhabung) ein noch höheres Potential für Injection-Angriffe bieten.
627 |
628 | Die erwartete Objekt-Struktur für `userData` Objekte wurde unter "Abschnitt 3.4 - Schritt 1" dargestellt, kann aber vernachlässigt werden, da sie bisher nicht mittels Eingabe-Validierung durchgesetzt wurde.
629 |
630 | Ein `userData` Objekt kann also beliebig gestaltet sein, solange die UTF-8 kodierte JSON-Repräsentation des Objektes, die Größe von 768 Bytes nicht überschreitet (nach Verschlüsselung noch immer 768 Bytes, nach Base64 Kodierung 1024 Bytes - wie vom Schema der REST API erzwungen).
631 |
632 | Im weiteren Verlauf sollen nur noch `userData` Objekte isoliert betrachtet werden (festgelegter Fokus). Der enge Betrachtungsfokus ist der "ungüstig komlexen" Code-Struktur geschuldet, ich kann aber versichern, dass an vielen Stellen im Code ähnliche Mängel auszumachen sind. Diese herauszuarbeiten "is up to the reader".
633 |
634 | Bisher wurde nur auf Luca-Code der Version `v1.1.11` verwiesen. Zum Zeitpunkt der Erstellung dieses Dokumentes, ist allerdings die Version `v1.2.1` im Produktionsbetrieb. Version `v1.2.1` (Release vom 02. Juni 2021) beinhaltet zusätzlich alle Patches zur öffentlich diskutierten "CSV Injection" Schwachstelle.
635 |
636 | **ABER: Alle bisher gemachten Feststellungen zur fehlenden Eingabe Validierung, treffen analog auf die aktuellste Version des Luca-Systems zu.**
637 |
638 | Auch in Version `v1.2.1` werden `userData` nach der Entschlüsselung nicht validiert. Siehe hierzu:
639 |
640 | 1. [Code Link v1.2.1 - Entschlüsselung/JSON-Parsing für App Nutzer](https://gitlab.com/lucaapp/web/-/blob/v1.2.1/services/health-department/src/utils/decryption.js#L122)
641 | 2. [Code Link v1.2.1 - Entschlüsselung/JSON-Parsing für Schlüsselanhänger](https://gitlab.com/lucaapp/web/-/blob/v1.2.1/services/health-department/src/utils/decryption.js#L47)). Hier wurde lediglich eine zusätzliche Prüfung eingebaut, um festzustellen ob bereits Nutzerdaten hinterlegt sind ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.2.1/services/health-department/src/utils/decryption.js#L44)). Dies hat keinen Einfluss auf die "freie Gestaltbarkeit" der `userData` Objekte, sondern erlaubt Schlüsselanhänger zu erkennen, die noch nicht durch einen Nutzer registriert wurden.
642 |
643 | Nach der Entschlüsselung werden die Daten, **noch immer ungefiltert**, durch die React Komponente `ContactPersonView` ([Code Link v1.2.1](https://gitlab.com/lucaapp/web/-/blob/v1.2.1/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.react.js#L58)) an die React Komponente `Headers` übergeben ([Code Link v1.2.1](https://gitlab.com/lucaapp/web/-/blob/v1.2.1/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.react.js#L98)). Dort dienen die Daten als "Single-Source-of-Thruth" für jedeweitere Verarbeitungen, vollkommen unabhängig vom Ausgabe-Kontext.
644 |
645 | Zusammenfassend kann man sagen:
646 |
647 | Das **"Health-Department Frontend" erstellt JavasScript Objekte (`userData`) aus JSON-Eingabedaten die ausschließlich der Kontrolle nicht vertrauenswürdiger Nutzer unterliegen**. Diese Javascript-Objekte durchlaufen verschiedene Kontexte, **ohne jegliche Filterung** (keine zentrale Eingabe-Validierung, keine Ausgabe-Kodierung). Dies gilt auch noch für die aktuellste Release-Version `v1.2.1`.
648 | Eine Filterung kann nur noch in den finalen Ausgabekontexten erfolgen. Die bisher fehlende **Eingabe-Validierung, muss dazu redundant, für jeden Kontext implementiert sein**.
649 |
650 | ## 3.6.1 Output-Kontext CSV-Export
651 |
652 | Vorbemerkung: dieser Abschnitt ist ausfühlich gehalten. Der CSV-Export dient hier als **exemplarisches Beispiel** für wiederkehrende Probleme im Luca-Code. Im speziellen soll analytisch dargelegt werden, dass das Luca-System (Implementierungs-bedingt) externe Eingabedaten **nicht** so filtern kann, dass Angriffe über Nutzereingaben verhindert werden. Dieser Abschnitt betrachtet konkret den Angriffsvekor "CSV Injection" als eine Untermenge von "Code Injection" Angriffen. Dennoch werden hier Programmierfehler angesprochen, welche sich analog in anderen Teilen des Luca-Codes (und anderen Luca-Systemkomponenten) wiederfinden. Diese Fehler "verbreitern" jeweils die Angriffsfläche über "Code Injection"-Schwachstellen.
653 |
654 | ## 3.6.1.1 Vorbetrachtung Output-Kontext CSV-Export: **Warum Luca bei der Filterung von Nutzerdaten versagen muss** (zusätzlich: Definition der Anforderungen an Ein-/ausgabefilterung für `v1.1.11`)
655 |
656 | Wie am Ende des Abschnittes 3.4 dargestellt, wird das `traces` Array mit den nicht validierten `userData` Objeketen von der React Komponente `Header` and die React Komponente `CSVDownload` weitergegeben: [Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/Header/Header.react.js#L50)
657 |
658 | Die Komponente `CSVDownload` wird in `ContactPersonView.helper.js` implementiert: [Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.helper.js#L313)
659 |
660 | ```
661 | export const CSVDownload = ({ traces, location }) => {
662 | const intl = useIntl();
663 | return (
664 |
668 | Download CSV
669 |
670 | );
671 | };
672 | ```
673 |
674 | Die erstellung des CSV-Outputs wird an eine weitere React Komponente `CSVLink` delegiert.
675 |
676 | **Die `CSVLink` Komponente gehört aber nicht zum Luca-Code, sondern wird von der externe React-Library `react-csv` bereitgestellt. Das Luca-System kann hier also gar kein CSV-bezogenes Output-Encoding mehr vornehmen. Die `CSVLink` Komponente, der externe `react-csv` Library, stellt dabei die Ausgabedaten direkt an den Nutzer bereit (die CSV Datei, welche im Gesundheitsamt heruntergeladen wird) und übernimmt damit die Aufgabe des Output-Encodings und - Escapings. Darüber hinaus, hat diese Library nun die alleinige Kontrolle über den Output, welcher dem Gesundheitsamt bereitgestellt wird.**
677 |
678 | **Ob diese Library erwartungsgemäß funktioniert, kann nur durch Tests beantwortet werden. Der Luca-Quellcode lässt aber keine diesbezüglichen Tests erkennen. Damit werden Datenschutz- und Sicherheitskritische Funtionalitäten ungetestet nach "Extern" ausgelagert.**
679 |
680 | Ohne Funktions- und Sicherheitstests könnte man die Verlässlichkeit solcher externen Libraries eventuell noch grob abschätzen (wenn man denn so etwas wie "Dependency Management" betreibt und dabei "Security" nicht aus den Augen verliert). VErsuch man dies, lässt sich zur `react-csv` Library folgendes feststellen:
681 |
682 | - Luca in der Version `v1.1.11` verwendet `react-csv 2.0.3` [(Code Link)](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/package.json#L36)
683 | - Luca in der aktuellsten Version `v1.2.1` verwendet ebenfalls `react-csv 2.0.3` [(Code Link)](https://gitlab.com/lucaapp/web/-/blob/v1.2.1/services/health-department/package.json#L38)
684 | - `react-csv` hat seit 01. April 2020 keine Updates mehr erfahren, `v2.0.3` war die letzte veröffentlichte Version ([Link](https://github.com/react-csv/react-csv/releases/tag/v2.0.3))
685 | - Man könnte annehmen, die Library wäre so weit gereift, dass keine Updates mehr nötig sind, aber:
686 | - Es warten **über 30 Pull Requests**, welche überwiegend **weitere externe Abhängikeiten auf den aktuellen Release-Stand bringen sollen** ([Link](https://github.com/react-csv/react-csv/pulls)). Die "Dependency Hell" kann hier nicht als Entschuldigung dienen. Erneut gibt man grob fahrlässig Verantwortung und Sicherheit auf.
687 | - `react-csv` pflegt **über 70 offene issues** - ganz allein, ohne Betrachtung der weiteren Dependencies-Updates die in Pull Requests auf Umsetzung warten ([Link](https://github.com/react-csv/react-csv/issues)). Unter anderem Pflegt man hier auch das Thema "CSV Injection" seit über 2 Jahren als offenen Issue ([Link](https://github.com/react-csv/react-csv/issues/156)).
688 |
689 | **Halten wir fest:**
690 |
691 | ---
692 |
693 | Output-Encoding für CSV kann von Luca gar nicht vorgenommen werden, da der Output außehalb des Luca-Codes erstellt wird.
694 |
695 | Die fehlende Input Validation nachzuholen, wäre an dieser Stelle noch denkbar, aber deplaziert, denn: Man müsste die exakt selbe Eingabe-Validierung (der `userData` Objekte) für jeden Ausgabe-Kontext redundant implementieren. Außerdem, müsste jeder bereits von den Daten durchlaufene Kontext (Kontextwechsel zu JavaScript, Kontextwechesel zu React ...) trotzdem auf diese Eingabe-Validierung verzichten.
696 |
697 | Die Luca-Entwickler haben zwar bewiesen, dass sie durchaus gewillt sind mit redundantem Code zu arbeiten, aber auch, dass dieser dann unterschiedlich schlecht funktioniert.
698 |
699 | ---
700 |
701 | Wo findet sich nun die Umsetzung der OWASP Empfehlungen zu "CSV Injection" statt (welche zeitgleich zum Update auf Luca `v1.1.11` als vorhanden beworben wurde)?
702 |
703 | Nun, die Daten die an externe `CSVLink` Komponente weiter gegeben werden, durchlaufen (wie im Code Auszug dargestellt) zunächst die Funktion `getCSVDownloadDataFromTraces` ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.helper.js#L317)).
704 |
705 | Innerhalb der `getCSVDownloadDataFromTraces` durchläuft jedes `userData` Objekt durch die Funktion `filterTraceData` ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.helper.js#L272)):
706 |
707 | ```
708 | ...traces
709 | .map(({ userData, isDynamicDevice, ...other }) => ({
710 | ...other,
711 | userData: filterTraceData(userData, isDynamicDevice),
712 | }))
713 | ```
714 |
715 | Ich hoffe es ist deutlich geworden, dass es sich hier nicht mehr um getrennte Eingabe-Validierung und Kontext-basiertes Ausgabe-Encoding handeln kann. Dieser "Filter" kann bestenfalls noch als "Sanitizer" implementiert sein. Ein solcher Ansatz birgt allerdings Risiken und Nachteile, vor allem aber ist es weitaus schwierigiger, mit einem "Sanitizer" zu einer funktionierenden Umsetzung zu kommen und diese geeignet zu testen. Vor der tatsächlichen Betrachtung der `filterTraceData` Funktion, möchte ich die Ansätze "Input Validierung + Output Encoding" und "Sanitization" kurz gegenüberstellen.
716 |
717 | Ich nutzer dafür einen Bildhaften vergleich:
718 |
719 | ---
720 |
721 | _Stellen sie sich die Wasserversorgung in einem Haus vor. Es gibt, verteilt über die Etagen, mehrerer Wasserhähne (Output-Kontext). Die Wasserhähne sollen, je nach Bedarf, Wasser in der gewünschten Temperatur liefern. Die Wunschtemperatur wird über die jeweiligen Mischbatterien der Wasserhähne geregelt (kontextbasiertes Output-Encoding)._
722 |
723 | _Damit die Wasserhähne nicht nur kaltes Wasser liefern, benötigen sie jeweils Warmwasser, mit einer definierten Minimaltemperatur (valider Input). Die Bereitstellung des Warmwassers wird, für das gesamte Haus, durch einen Warmwasserboiler gewährleistet. Dieser wird zentral am Kaltwasseranschluss platziert. Der Boiler nimmt Kaltwasser auf (unvalidierter Input) und stellt sicher, dass dieses - vor weiterleitung - immer auf die gewünschte Minimaltemperatur gebaracht wird (Input Validierung)._
724 |
725 | _Die Analogie für "Sanitization" würde so aussehen:_
726 |
727 | _Es kommen nur noch Wasserhähne ohne Mischbatterie zum Einsatz (kein Output-Encoding). An jedem Wasserhahn kommt Kaltwasser, mit schwankender Temperatur an (unvalidierter Input). Um trotzdem an jedem Hahn die geünschte Wassertemperatur abzugreifen, muss jeweils ein Durchlauferhitzer angebracht werden (Sanitizer). Die Wunschtemperatur bestimmt jetzt der Durchlauferhitzer (Sanitization), dieser muss aber regelmäßig nachjustiert werden, da das zulaufende Kaltwasser in der Temperatur schwankt (unvalidierter Input). Wird vergessen, an einem Hahn einen Durchlauferhitzer zu platzieren, liefert der Hahn nur Kaltwasser mit schwankender Temperatur (unkontrollierter Output). Ist einer der Durchlauferhitzer fehlerhaft (Fehler im Sanitizer), merken Sie dies erst, wenn sie sich am zu heißen Wasser die Hände verbrennen. Haben Sie nicht den gesammten Wasserkreislauf des Hauses im Blick und es werden versehentlich zwei Durchlauferhitzer in Folge durchlaufen ("double encoding"), ist das Wasser nicht zu kalt, aber sie können sich erneut die Hände verbrennen._
728 |
729 | ---
730 |
731 | Zusammengefasst: "Sanitization" ist ein "sub-optimaler" Ansatz mit vielen Nachteilen (Wartungsintensiv, Testintensiv, muss an allen wichtigen Stellen im Datenfluss unterschiedlich Implementiert werde - je nach Kontext, ist Fehleranfällig, lässt sich häufig Umgehen).
732 |
733 | Zurück zur `filterTraceData` Funktion.
734 |
735 | Wie erläutert, kann dieser "Filter" - aufgrund seiner logischen Positionierung im "Datenfluss" - nur noch als Sanitizer angelegt sein (als "Durchlauferhitzer").
736 | Der Filter muss nicht nur "nachholen", was bisher im Bereich Input Validierung versäumt wurde, der Filter muss zusätzlich das Encoding/-Escaping für seinen Ausgabe-Kontext sicherstellen.
737 |
738 | Zur Erinnerung: **Der Ausgabe-Kontext ist NICHT "CSV (Character-separated values)". Der Ausgabe-Kontext der `filterTraceData` Funktion sind Daten, die an die React Komponente `CSVLink` übergeben werden (als neuer Input).**
739 |
740 | Demgegnüber steht die Behauptung des Luca-Herstellers, dass "... beim Entschlüsseln der Daten die OWASP Empfehlungen zu CSV Injections ..." umgesetzt werden (vergl. Abschnitt 3.3). Das keine Filterung beim "Entschlüsseln der Daten" vorgenommen wird, muss nicht wiederholt werden. Wichtig für die Implementierung `filterTraceData` ist, dass die Aufgabe darin besteht die OWASP-Empfehlungen zu "CSV Injection" umzusetzen.
741 |
742 | Auszug aus den OWASP Empfehlungen ([Link](https://owasp.org/www-community/attacks/CSV_Injection))
743 |
744 | ```
745 | When a spreadsheet program such as Microsoft Excel or LibreOffice Calc is used to open a CSV, any
746 | cells starting with = will be interpreted by the software as a formula. Maliciously crafted
747 | formulas can be used for three key attacks:
748 |
749 | - Hijacking the user’s computer by exploiting vulnerabilities in the spreadsheet software,
750 | such as CVE-2014-3524.
751 | - Hijacking the user’s computer by exploiting the user’s tendency to ignore security warnings
752 | in spreadsheets that they downloaded from their own website.
753 | - Exfiltrating contents from the spreadsheet, or other open spreadsheets.
754 |
755 | This attack is difficult to mitigate, and explicitly disallowed from quite a few bug bounty programs.
756 | To remediate it, ensure that no cells begin with any of the following characters:
757 |
758 | - Equals to (=)
759 | - Plus (+)
760 | - Minus (-)
761 | - At (@)
762 | - Tab (0x09)
763 | - Carriage return (0x0D)
764 |
765 | ...
766 |
767 | Alternatively, prepend each cell field with a single quote, so that their content will be read as
768 | text by the spreadsheet editor.
769 |
770 | ```
771 |
772 | ### Kurzanalyse der OWASP Empfehlung "CSV Injection", als Anforderungsdefinition für `filterTraceData`:
773 |
774 | ---
775 |
776 | Znuächst legt die Empfehlung den Ausgabe-Kontext fest, in dem sie gilt: **"a spreadsheet program"** (ein Tabellenkalkulationstabellenkalkulationsprogramm)
777 |
778 | Weiter wird festgelegt, welche Teile der Input-Daten zu Filtern sind: "... cells starting with ...", also CSV-Daten welche von der Tabellenkalkulation als **Zellen** interpretiert werden.
779 |
780 | Die ungefilterter Ausgabe müsste also zunächst in Zellen zerlegt werden, um überhaupt eine Filterung ansetzen zu können. Diese Zellenzerlegung, müsste in der Logik folgen, welche die Tabellenkalkulation zur Zellenzerlegung ansetzt.
781 |
782 | CSV ist ein nicht sauber standardisiertes Format, welches trotzdem breite Anwendung findet. Die spiegelt sich wieder, wenn man zunächst versucht abzuleiten, wie eine Zelle überhaupt definiert ist.
783 | Ich habe CSV zuvor bewusst als **"Character-separated values"** ausformuliert, weitaus gebräuchlicher ist allerdings die Bezeichnung **"Comma-separated values"**. In der Regel wir ein `,` (ASCII `0x2C`) als Zellentrenner verwendet. Für alle gängigen Tabellenkalkulationen ist diese Interpretation eines Zellentrenners in CSV-Eingabedaten auch der "Default". Dennoch kann grundsätzlich jedes ASCII-Zeichen als Zellentrenner festgelegt werden und wäre dann im Zelleninhalt so zu escapen, dass es **nicht als Zellentrenner interpretiert wird**.
784 |
785 | _Häufig wird auch `;` (ASCII `0x3B`, bspw. für den SORMAS Export) oder ein `tab` (ASCII `0x09`) als Zellentrenner verwendet._
786 |
787 | RFC4180 ([Link](https://datatracker.ietf.org/doc/html/rfc4180)) versucht die CSV-Variante formal zu beschreiben, welcher die meisten Implementierungen folgen, macht dabei allerdings deutlich, dass es keine einheitliche Spezifikation gibt. Da auch Tabellenkalkulationen ("per Default") diese Interpretation anlegen, soll diese als Betrachtungsgrundlage dienen. Zunächst heißt das, dass **ein Zellentrenner immer ein Komma ist.**
788 |
789 | Weiter definiert der RFC4180 die CSV-Syntax wie folgt:
790 |
791 | ```
792 | file = [header CRLF] record *(CRLF record) [CRLF]
793 | header = name *(COMMA name)
794 | record = field *(COMMA field)
795 | name = field
796 | field = (escaped / non-escaped)
797 | escaped = DQUOTE *(TEXTDATA / COMMA / CR / LF / 2DQUOTE) DQUOTE
798 | non-escaped = *TEXTDATA
799 | COMMA = %x2C
800 | CR = %x0D
801 | DQUOTE = %x22
802 | LF = %x0A
803 | CRLF = CR LF
804 | TEXTDATA = %x20-21 / %x23-2B / %x2D-7E
805 | ```
806 |
807 | Was ist hier relevant zur Umsetzung der OWASP-Empfehlungen:
808 |
809 | 1. **Zellen** sind hier als `field` definiert und der **Zelleninhalt** kann in zwei Varianten vorliegen: `escaped` oder `non-escaped`
810 | 2. Für `non-ecaped` Zellen sind als Inhalt nur die **druckbaren** ASCII Zeichen `0x20-0x7E` zulässig. Die Zeichen `"` (`0x22`) und `,` (`0x2C`) sind **explizit verboten**
811 | 3. Für `escaped` Zellen sind die Inhalte mit `"` (`0x22`) zu umschließen. Die `"` gehören selbst nicht zum Zelleninhalt, da sie dem escaping dienen. Als Zelleninhalt wären dann auch die Zeichen `,` (`0x2C`), `LF` (`0x0A`) un `CR` (`0x0D`) erlaubt. Die Verwendung des Zeichens `"` (`0x22`) ist allerdings auch hier nur als Doppelfolge `""` erlaubt.
812 | 4. Eine **Tabellenzeile** (`record` / `header`) besteht aus einer einzelnen **Zelle** oder einer **Reihe von Zellen** die durch ein `,` (`0x2C`) als Zellentrenner abgegrenzt werden.
813 | 5. Enthält die Tabelle mehr als eine Zeilen, werden die \*\*Tabelleinzeilen durch `CRLF` (`[0x0D, 0x0A]`) abgegrenzt.
814 | 6. Die Tabellenzeilen `header` und `record` unterscheiden sich nur formal, nicht syntaktisch.
815 |
816 | Die OWASP Empfehlungen beziehen sich auf das **erste Zeichen einer Zelle** und machen dabei konkrete Vorgaben, welche Zeichen zu unterdrücken oder durch Voranstellen eine `'` (`0x27`) zu escapen sind: `[=+-@]`, `0x0D` und `0x0A`.
817 |
818 | Die Umsetzung der OWASP Empfehlung für beliebigen Input bis zur Konvertierung einer RFC4180 konformen **CSV-Zelle**, könnte etwa so aussehen (Pseudo-Code, ohne Error-Handling, Length-Checks etc):
819 |
820 | ```
821 | // Input validation according to RFC4180
822 | ByteArray validateCsvCellInputRFC(BytaArray inputCellContent, Bool srict) {
823 | ByteArray output = [];
824 |
825 | // printable ASCII, without ',' and '"'
826 | ByteArray allowedBytes = [0x20..0x21, 0x23..0x2B, 0x2d..0x7e];
827 |
828 | // encodable/escapable characters DQUOTE, COMMA, LF, CR
829 | ByteArray encodableBytes = [0x22, 0x2c, 0x0a, 0x0d];
830 |
831 | for (int i = 0; i < inputCellContent; i++) {
832 | Byte textdata = inputCellContent[i];
833 | if (allowedBytes.contains(textdata) || encodableBytes.contains(textdata)) {
834 | output.append(textdata);
835 | } else if (strict) {
836 | throw InvalidCsvCellDataException(textdata);
837 | }
838 |
839 | // if here, invalid byte gets dropped (not appended to output)
840 | }
841 |
842 | return output;
843 | }
844 |
845 | // Output encoding according to RFC4180
846 | ByteArray encodeCsvCellOutputRFC(BytaArray csvCellContent, Bool escape) {
847 | // Input was validated according to RFC4180, thus it could be handled as ASCII
848 | String strField = csvCellContent.decode("ASCII");
849 |
850 | // check if characters exist, which require an 'encoded' field according to RFC4180
851 | // an encoded field has to be enclosed with '"'
852 | Bool requiresEnclosing = strField.containsOneOf(['"', ',', '\n', '\r']));
853 |
854 | // special case: escape DQOUTES '"' to 2DQUOTES '""'
855 | strField = strField.split('"').join('""');
856 |
857 | // add enclosing if required
858 | if (requiresEnclosing) strField = '"' + strField + '"'
859 |
860 | // return result as ASCII-encoded ByteArray
861 | return strField.encode("ASCII")
862 | }
863 |
864 | // Input validation according to OWASP suggestion for "CSV Injection"
865 | ByteArray validateCsvCellInputOWASP(BytaArray validInputCellContent, Bool srict) {
866 | // handover to OWASP-agnostic encoding, if no strict validation is required
867 | if (!strict) return validInputCellContent;
868 |
869 | // forbidden, but still escapable during output encoding ([=+-@], 0x0D, 0x0A)
870 | ByteArray forbiddenFirstCharForCell = [0x3d, 0x2b, 0x2d, 0x40, 0x0d, 0x0a];
871 |
872 | if (forbiddenFirstCharForCell.contains(validInputCellContent[0])) {
873 | throw ForbiddenFirstCellCharException(validInputCellContent[0]);
874 | }
875 |
876 | return validInputCellContent;
877 | }
878 |
879 | // Output Encoding according to OWASP suggestion for "CSV Injection"
880 | ByteArray encodeCsvCellOutputOWASP(BytaArray csvCellContent, Bool escape) {
881 | // forbidden, but still escapable during output encoding ([=+-@], 0x0D, 0x0A)
882 | ByteArray forbiddenFirstCharForCell = [0x3d, 0x2b, 0x2d, 0x40, 0x0d, 0x0a];
883 |
884 | // if cell-content starts with forbidden character, as defined by OWASP
885 | if (forbiddenFirstCharForCell.contains(csvCellContent[0])) {
886 | // if escaping is enabled, prefix with "'"
887 | if (escape) return ByteArray.concat([0x27], csvCellContent);
888 |
889 | // if escaping is disabled, drop forbidden first character
890 | return csvCellContent.trim(1, csvCellContent.Length); // drop first character
891 | }
892 |
893 | return csvCellContent; // no further encoding required
894 | }
895 |
896 | // filters unsanitized Input to RFC4180 compliant CSV-Field with "CSV Injection" prevention according to OWASP
897 | ByteArray filterCellInputForCSV(ByteArray rawCellInput, Bool strict, Bool escapeOWASP) {
898 | // Input validation according to RFC4180
899 | ByteArray validCsvCellContent = validateCsvCellInputRFC(rawCellInput, strict);
900 |
901 | // Input validation according to OWASP suggestion for "CSV Injection"
902 | ByteArray nonEncodedCsvCellContent = validateCsvCellInputOWASP(validCsvCellContent, strict);
903 |
904 | // Encode Output according to OWASP (by escaping forbidden characters or dropping them)
905 | ByteArray encodedCellContentOWASP = encodeCsvCellOutputOWASP(validCsvCellContent, escapeOWASP); // escape forbidden characters
906 |
907 | // Encoding Output according to RFC4180
908 | return encodeCsvCellOutputRFC(validCsvCellContent);
909 | }
910 |
911 | ```
912 |
913 | Der Pseudo-Code zur Ein- und Ausgabefilterung von CSV-Zellen, soll herausstellen, warum die Luca-Implementierung in `filterTraceData` nicht effektiv arbeiten kann (auch wenn dei konkrete Implementierung noch nicht betrachtet wurde).
914 |
915 | Die Pseudo-Code verhindert CSV-Injection auf **"Zellen-Ebene"**. Damit die OWASP-Filterung wie vorgeshen erfolgen kann, müssen die Eingangsdaten als **Zellinhalte** vorliegen, denn nur hier lassen sich die Vorgaben anwenden. Die `filterTraceData` Funktion aus dem Luca Code verarbeitet allerdings (beliebige) JavaScript Objekte (`userData`), also nicht Zellinhalte.
916 | Die `filterTraceData` Funktion könnte bestenfalls OWASP-konforme **Zelleninhalte** ausgeben, die Weiterverarbeitung wird allerdings von der `CSVLink` Komponente der `react-csv` Library übernommen. Das wiederum heißt: Die `CSVLink` Komponente, verarbeitet die Eingabedaten der `filterTrace` Funktion zunächst zu CSV-Zellen und letztendlich zu CSV. Dies ist in sofern problematisch, als das die `filterTraceData` Funktion nun gar nicht mehr zwischen Eingabedaten die als CSV-Zelleninhalte fungieren und der erzeugten CSV-Ausgabe ansetzen kann. Der Pseudo-Code stellt das Problem ebenfalls dar:
917 |
918 | Die Funktionen `validateCsvCellInputOWASP` und `encodeCsvCellOutputOWASP` setzen die OWASP-Filterung um. Allerdings **muss diese Filterung zwischen** der RFC4180-konformen **Eingabeverarbeitung** (`validateCsvCellInputRFC`) und der RFC4180-konformen **Ausgabeverarbeitung** (`encodeCsvCellOutputRFC`) erfolgen. Nur so kann überhaupt sichergestellt werden, dass die Inhalte von CSV-Zellen gefiltert werden, auf die sich die OWASP-Empfehlungen beziehen. Analog dazu, müsste die `filterTraceData` Funktion aus dem Luca-Code innerhalb der `react-csv` Library arbeiten, nämlich da wo valide CSV-**Zellen**inhalte vorliegen, aber bevor diese Inhalte zu CSV-Dateien weiterverarbeitet werden. Da die `filterTraceData` Funktion aber logisch **vor der CSV-Verarbeitung** arbeitet, muss sie zusätzliche Anforderungen erfüllen, um die OWASP-Empfelungen umzusetzen:
919 |
920 | - Validierung der Eingabedaten `userData` (Der Sicherheits-Effekt ist beschränkt, da eine Verarbeitung der unvalidierten Eingaben bereits in anderen Kontexten erfolgt ist. Die Eingabe-Validierung an dieser Stelle erzielt keinen Nutzen für andere Ausgabe-Kontexte - wie bspw. SORMAS Rest Schnittstelle - mehr und müsste dort erneut erfolgen).
921 | - Genaue Festlegung, welche der Eingabedaten (`userData`) in der Weiterverarbeitung durch die `CSVLink` Komponente der `react-csv` Library, als **Zelleninhalte** angesehen werden. Die genaue Art und Weise der Weiterverarbeitung durch `react-csv` (und weiterer Libraries welche von `react-csv` eingebunden werden) muss dabei so analysiert werden, dass für **alle möglichen Eingabedaten** feststeht, wo und wie diese als Zellen interpretiert werden (deterministisch).
922 | - Das Ausgabe-Encoding für CSV-Zelleninhalte (vergleiche Analogie im Pseudo-Code-Beispiel: `encodeCsvCellOutputRFC`) wird nun Anteilig von der Luca Funktion `filterTraceData` und von der internen Funktionalität der `react-csv` Library übernommen. Die dazu muss genau definiert sein. Funktionsanpassungen in `react-csv` müssen zu Funktionsanpassungen in der Luca Funktion `filterTraceData` führen, um sicher zu stellen, dass diese Schnittstelle klar definiert bleibt. So muss zum Beispiel klar sein, welche der beiden Komponenten das Encoding/Escaping der CSV-Zellen übernimmt.
923 | - Nach Anwendung der Filterfunktionalität bezüglich der OWASP Empfehlungen, muss die `filtertraceData` Funktion aus dem Luca-code zusätzlich sicherstellen, dass es durch die `react-csv` Library nicht zu Veränderungen an den Eingangsdaten kommt, die die Filter wieder unwirksam machen oder zu funktionalen Problemen führen (z.B. Double-Encoding).
924 |
925 | Zusammengefasst ergeben sich Anforderungen an die `filterTraceData` Funktion, die nur erfüllt werden können, wenn die Weiterverarbeitung der Daten, bis zum letztendlichen Export als CSV-Datei, vollständig vom Luca-Code kontrolliert wird. Mit der Verwendung einer externen Library (`react-csv`) kann dies nicht sichergestellt werden, noch kann ein Filter gegen "CSV Injection" an einer Stelle platziert werden, an der dieser Wirkung entfaltet.
926 |
927 | ---
928 |
929 | ## 3.6.1.2 Output-Kontext CSV-Export: Umsetzung der Filterung für "CSV Injections" in `filterTraceData` (`v1.1.11`)
930 |
931 | Die ursprüngliche `filterTraceData` Funktion wurde [hier (Code Link)](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/utils/sanitizer.js#L32) implementiert:
932 |
933 | ```
934 | import { pick } from 'lodash';
935 | import {
936 | assertStringOrNumericValues,
937 | escapeProblematicCharacters,
938 | } from './typeAssertions';
939 |
940 | const staticDeviceDataPropertyNames = [ 'fn', 'ln', 'pn', 'e', 'st', 'hn', 'pc', 'c', 'vs', 'v', ];
941 |
942 | const dynamicDevicePropertyNames = ['fn', 'ln', 'pn', 'e', 'st', 'hn', 'pc', 'c', 'v', ];
943 |
944 | export function filterTraceData(userData, isDynamicDevice) {
945 | const picked = escapeProblematicCharacters(
946 | pick(
947 | userData,
948 | isDynamicDevice
949 | ? dynamicDevicePropertyNames
950 | : staticDeviceDataPropertyNames
951 | )
952 | );
953 | assertStringOrNumericValues(picked);
954 | return picked;
955 | }
956 |
957 | ```
958 |
959 | Zunächst ruft die `filterTraceData` Funktion eine weitere Funktion namens `escapeProblematicCharacters`.
960 | Die Funktion `escapeProblematicCharacters` erhält allerdings nicht mehr das vollständige `userData` Objekt, sondern nur noch eine Kopie des Objektes, welche nur noch die eigentlich vorgesehenen Objekt-Eigenschaften (Properties) enthält, die für "Kontaktdaten" existieren dürfen.
961 |
962 | _Vorgesehen sind die Objekt-Keys aus `staticDeviceDataPropertyNames` für Kontaktdaten von Schlüsselanhängern, bzw. aus `dynamicDevicePropertyNames` für Kontaktdaten aus der Luca-App (siehe auch Abschnitt 3.4, Schritt 1)._
963 |
964 | Zur Auswahl der zulässigen `userData` Objekteigenschaften wird die Funktion `pick` der Library `loadsh` genutzt. Es wurde bereits ausführlich dargelegt, dass diese (semantische) Eingabe-Filterung viel früher vorzunehmen wäre. Als Beispiel wurden Angriffsszenarien wie "Prototype Pollution" benannt, die bereits vorher bei der Datenverarbeitung im JavaScript-Kontext zum Tragen kommen könnten (auch die `loadsh` Library war von "Prototype Pollution" schon betroffen). Weiter wurde dargelegt, dass andere Kontexte von dieser Filterung nicht mehr profitieren, sofern diese nicht redundant implementiert wird. So würde bswp. für den CSV-Export eine JavaScript-Objekt-Eigenschaft wie `userData.__proto__ = ` verworfen, im Verarbeitungs-Kontext für die visuelle Darstellung in der Web-Applikation bleibt sie allerdings erhalten. Klar ist auch, dass **die Werte (Values)** von zulässigen Objekteigenschaften noch immer Schaden in der weiteren Verarbeitung anrichten können. Die Funktion `escapeProblematicCharacters` musss diesem Risiko begegnen, denn sie nimmt die weitere Filterung an der Kopie des `userData` Objektes vor.
965 |
966 | Die Werte für zulässige Objekteigenschaften, können bisher noch immer beliebig gestaltet sein. Für die Postleitzahl aus den Kontaktdaten, wären beispielsweise folgende Werte zulässig (zur Erinnerung: Nahezu alle Werte werden als String erwartet, wie in Abschnitt 3.4 dargestellt).
967 |
968 | ```
969 | // Postleitzahl mit führender 0 als String (angenommene Eingabe)
970 | userData.pc = "01337"
971 |
972 | // Postleitzahl vom Type 'number', führende 0 wird vernachlässigt
973 | userData.pc = 01337
974 |
975 | // Postleitzahl vom Typ 'number' als '-Infinity' um Fehler zu provozieren
976 | // aus `JSON.parse('{"p": -1e500 , ...}'))`
977 | userData.pc = -Infinity
978 |
979 | // nested Object
980 | userData.pc = {"__proto__": {"__proto__": {"foo": "bar"}}}
981 |
982 | ...
983 | ```
984 |
985 | Auch können Objekteigenschaften fehlen (das Vorhandensein eines Vornamens wird bswp. vor der Weiterverarbeitung nicht geprüft).
986 |
987 | Erst nach der `escapeProblematicCharacters` Funktion, wird auf das neue Objekt die Funktion `assertStringOrNumericValues` angewendet, welche dem Namen nach für korrekt typisierte Object-values sorgen soll (Typ `string` oder `number`).
988 |
989 | Auch hier entbehrt die Codeabfolge einer gewissen Logik, sofern man annimmt, dass die `escapeProblematicCharacters` Funktion nur auf die Datentypen angewendet werden soll, die als Properties im `userData` Objekt erwartet werden (also der Typ `string` für alle Kontaktdaten-Attribute und der Tpy `number` für die Property namens `"v"`, welche die Version des Kontaktdatenobjektes speichert).
990 |
991 | Die beiden Funktionen `escapeProblematicCharacters` und `assertStringOrNumericValues` sind gesondert im Modul `typeAssertions.js` definiert ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/utils/typeAssertions.js#L3)):
992 |
993 | ```
994 | import assert from 'assert';
995 |
996 | export function assertStringOrNumericValues(object) {
997 | Object.values(object).forEach(value => {
998 | assert(typeof value === 'string' || typeof value === 'number');
999 | });
1000 | }
1001 |
1002 | export function escapeProblematicCharacters(object) {
1003 | const target = {};
1004 | Object.entries(object).forEach(([key, value]) => {
1005 | if (typeof value === 'string') {
1006 | const valueWithReplacedPlus = value.replace('+', '00');
1007 | target[key] = valueWithReplacedPlus.replace(/^([=-@\t\r])/, "'$1");
1008 | } else {
1009 | target[key] = value;
1010 | }
1011 | });
1012 | return target;
1013 | }
1014 | ```
1015 |
1016 | Die Funktion `assertStringOrNumericValues` tut was sie laut ihres Namens tun soll: Sie stellt sicher, dass alle Werte für die Properties des übergebenen Objektes den Typ `string` oder `number` haben.
1017 |
1018 | Die Funktion `escapeProblematicCharacters`, welche innerhalb von `filterTraceData` **vor der** `assertStringOrNumericValues` Funktion aufgerufen wird, muss aber noch aber noch mit Projekt-Properties beliebiger Typen arbeiten. Das macht aus logischer Sicht wenig Sinn, aus Sicherheitssicht kan es sogar hoch problematisch werden. Eine Erläuterung:
1019 |
1020 | Die Funktion `escapeProblematicCharacters` erwartet als Eingabeparameter ein beliebiges JavaScript-Objekt (der Parameter ist sogar als "objekt" benannt, um dies zu verdeutlichen). Die Funktion ist weiter logisch getrennt von der `filterTraceData` Funktion (welche in `sanitizer.js` definiert wurde, `escapeProblematicCharacters` gehört aber zu dem gesonderten Modul `typeAssertions.js`). Ich stelle dies aus genau einem Grund heraus: Die Funktion ist so generisch gestaltet, dass sie Entwickler dazu verleitet, sie auch an anderen Stellen im Code zu verwenden (außerhalb der `filterTraceData` Funktion). Der Funktionsname impliziert dabei, dass für die beliebigen Objekte, welche als Parameter übergeben werden können, "problematische Zeichen" gefiltert werden. Zeichen können aber nur in Objekt-Properties vom Typ String gefiltert werden! Was passiert also mit Properties, welche nicht vom Typ String sind?
1021 |
1022 | Die Codezeile `target[key] = value;` beantwortet diese Frage: Jede Objekt-Property, deren Wert (`value`) nicht die Bedingung `if (typeof value === 'string')` erfüllt, wird ohne weitere Änderungen an das neue Objekt `target` zugewiesen. Der Name der jeweiligen Property (`key`) wird dabei unverändert übernommen.
1023 |
1024 | Im Abschnitt 3.4 (unter Schritt 2), habe ich in einer Randbemerkung den Angriffsvektor "Prototype Pollution" angeschnitten. Dabei wurde festgehalten, dass Objekte die mittels `JSON.parse()` erzeugt werden, nicht allein für "Prototype Pollution" genutzt werden können. Es braucht ein "Sprungbrett" welches die relevanten Objekt-Properties über sogenannte **Setter** bedient. Die `escapeProblematicCharacters` Funktion "baut ein solches Sprungbrett". Würde sie an einer falschen Stelle eingesetzt, kann sie einem Angriff zum Erfolg verhelfen (Auswirkungen können vom Denial-of-Service bis zur Remote-Code-Execution im "Health-Department Frontend" reichen).
1025 |
1026 | Auch diese "Sprungbrett" Funktion soll kurz erläutert werden (Für ausführliche Informationen zum Vektor "Prototype Pollution" empfehle ich eine Internetrecherche. Für Software Projekte der tragweite von Luca, wird solcher Code eigentlich im Rahmen von Sicherheitstests, -audits und Code-Reviews ausgemerzt).
1027 |
1028 | **Exkurs: Prototype Pollution mittels `escapeProblematicCharacters`**
1029 |
1030 | ---
1031 |
1032 | JavaScript ist nicht wirklich eine objektorientierte Programmiersprache (OOP). Konstrukte wie "Klassen" oder "Instanzen" von Klassen (Objekte) werden nachgeahmt. Auch das Prinzip der Vererbung wird nachgeahmt. In einer OOP würde eine Klasse den Prototyp eines Objektes dieser Klasse beschreiben. Dieser beinhaltet Attribute ("Properties" in JavaScript) und Methoden ("functions" in JavaScript). Eine Klasse `Auto` könnte bspw. festlegen das jedes Auto ("Instanz" der Klasse) ein Attribut besitzt, welches die Anzahl der Räder als `radCount` speichert (dabei würde auch festgelegt, dass diese Anzahl immer den Typ Ganzzahl hat). Die Klasse `Auto` könnte auch eine Methode wie `Radwechsel` vorgeben, die Veränderungen an den Attributen vornimmt. Entscheidend ist, dass die Klasse nur einmal existiert! Jede Instanz der Klasse (ein Objekt), würde ein tatsächliches Auto repräsentieren. Diese Objekte belegen die vorgegebenen Attribute jeweils mit unterschiedlichen Werten. So kann es ein Objekt `pkw` der Klasse `Auto` geben, welches das Attribut `radCount` mit dem Wert `4` belegt (`pkw.radCount = 4`), aber auch ein Objekt `lkw` der geleichen Klasse, welches das gleiche Attribut mit `8` belegt (`lkw.radCount = 8`). Verschiedene Programmiersprachen setzen dies formal unterschiedlich um, allen gemein ist: Eine Klasse existiert nur **einmal** zentral angelegt, aber es kann (beliebig) viele Instanzen geben, die vorgegebenen Attribute unterschiedlich belegen. Auch die Methoden einer Klasse, werden im Regelfall nur einmal implementiert.
1033 |
1034 | Im Zusammenhang mit der "Prototype Pollution" sind für die JavaScript-Umsetzung von Objekt-Orientierung folgende Dinge wichtig:
1035 |
1036 | 1. Eine JavaScript Klasse (`prototype`) kann zur Laufzeit geändert werden. Die Änderungen wirken sich auf alle zukünftigen und bereits bestehenden Instanzen (`objetc`) der Klasse aus.
1037 | 2. Jede Klasseninstanz die vom Typ `object` erbt, enthält Attribute, welche auf die Klasse des Objektes verweisen (auf den `prototype`). Da der `prototype` veränderbar ist, können diese Attribute genutzt werden, um zur Laufzeit auf den `prototype` zuzugreifen (über ein entsprechendes Attribut eines `object`) und diesen nachhaltig zu verändern. Die Veränderungen wirken sich auf **alle** Instanzen aus, welche den selben `prototype` nutzen, **wenn dies nicht verhindert wird**. Eines der Attribute, welches für jede Instanz existiert die von der Klasse `object` erbt, und Zugriff auf den `prototype` erlaubt trägt den Namen `__proto__`.
1038 | 3. Ein JavaScript `object` verweist nicht nur auf seinen eigenen `prototype`, sondern der `prototype` verweist selbst auf Klassen von denen er geerbt hat. Da jedes JavaScript Objekt von der Klasse `object` erbt, ist es ohne entsprechende Vorkehrungen potentiel möglich, ausgehend von einer beliebigen Objekt-Instanz auf den `prototype` der "Basisklasse" `object` zuzugreifen und diesen zu verändern. Eine solche Änderung würde sich **global** auf alle exitierenden Objekte auswirken. (Wir d hier vernachlässigt, da bereits die Manipulation **einer** Klasse eine beliebigen Types zu unvorhersehbaren Folgen führen kann, je nachdem wie die Instanzen der Klasse weiter verwendet werden)
1039 |
1040 | Ein Kurzbeispiel:
1041 |
1042 | ```
1043 | > obj1 = {}
1044 | > obj2 = {}
1045 | > obj1.toString()
1046 | '[object Object]'
1047 | > obj2.toString()
1048 | '[object Object]'
1049 | > obj1.__proto__.toString = function() {return "Klasse 1"}
1050 | [Function]
1051 | > obj1.toString()
1052 | 'Klasse 1'
1053 | > obj2.toString()
1054 | 'Klasse 1'
1055 | >
1056 | ```
1057 |
1058 | Das Beispiel legt zunächst zwei JavaScript-Objekte `obj1` und `obj2` an. Beide Objekte haben Zugriff auf eine Methode (`Function`) namens `toString`. Die Methode `toString` ist in der Klasse `Object` definiert. Sowohl `obj1`, als auch `obj2` imlementieren standardmäßig die Klasse `Object` (da mit der Kurzschreibeweise `{}` Instanzen dieses Typs erzeugt wurden). Die Property `__proto__` erlaubt dabei den **Zugriff auf die "Definition" der Klasse** `Object` **über eine Instanz der Klasse**: so kann mittels `obj1.__proto__.toString = ...` die Methode `toString` für die Klassendefinition (für den `prototype`) der Klasse `Object` überschrieben werden. Da hier nicht eine Methode der Instanz, sondern des Prototyps überschrieben wird, wirkt sich dies auch auf `obj2` aus (in diesem Fall sogar auf alle existierenden Objekte im Scope). Die `toString()` Methode welche standardmäßig den String `'[object Object]'` zurückgibt, gibt nun für **alle** Objekte den String `'Klasse 1'` zurück.
1059 |
1060 | Im Abschnitt 3.4 wurde erläutert, dass JavaScript Objekte welche aus JSON-Strings erzeugt werden (mittels `JSON.parse()`) eine "Prototype Pollution" **vorbereiten** können, wenn vor der Weiterverarbeitung keine Eingabe-Validierung stattfindet. Der Begriff "vorbereiten" deutet an, dass es für JSON-Daten einen weiteren Schritt geben muss, um einen "Prototype Pollution" zu ermöglichen. Warum zwei Schritte nötig sind (für JSON-Daten), soll zum Verständnis ebenfalls erlätert werden.
1061 |
1062 | ```
1063 | > obj1 = JSON.parse('{"__proto__": { "toString": "this is not a function" } }')
1064 | { __proto__: { toString: 'this is not a function' } }
1065 | > obj1.toString()
1066 | '[object Object]'
1067 |
1068 | > obj2 = { "__proto__": { "toString": "this is not a function either" } }
1069 | {}
1070 | > obj2.toString()
1071 | Uncaught TypeError: obj2.toString is not a function
1072 | ```
1073 |
1074 | Im obigen Code-Schnipsel wurde das JavaScript Objekt `obj1` mittels der `JSON.parse()` Funktion erstellt. Die JSON-Daten versuchen dabei, den Prototyp der Objekt-Klasse über die `__proto__` Property zu überschreiben. Das Ziel: Die `toString` Methode - deren Wert eigentlich den Typ `function` hat - mit einenm Wert des Typs `string` zu überschreiben. Würde dies funktionieren, würde der anschließende Versuch die `toString` Methode über `obj1.toString()` aufzurufen, zu einem Fehler führen, da es sich bei `toString` gar nicht mehr um eine aufrufbare `Fucntion` handeln sollte.
1075 | Das Code-Beispiel zeigt allerdings, dass der Aufruf `obj1.toString()` nach wie vor einen Wert zurückgibt (`'[object Object]'`), statt einen Fehler zu erzeugen.
1076 |
1077 | Im weiteren Verlauf des Code-Beispiels wird genau dieses Vorgehen für ein Objekt namens `obj2` wiederholt. Der Unterschied: Anders als für `obj1` wird `obj2` nicht über `JSON.parse`, sondern "direkt" definiert. Für `obj2` wird beim Versuch `obj2.toString()` aufzurufen auch der Fehler erzeugt, der provoziert werden sollte.
1078 |
1079 | Um den JavaScript-Exkurs nicht zu überdehnen, möchte ich die Erklärung kurz halten:
1080 |
1081 | Für `obj2` wird die Property `__proto__` intern über einen "Setter" angesprochen, man kann sich dies etwa so vorstellen (pseudo Code):
1082 |
1083 | ```
1084 | // obj2 = { "__proto__": { "toString": "this is not a function either" } }
1085 | obj2.setPrototypeOf({ "toString": "this is not a function either" })
1086 | ```
1087 |
1088 | Die Property `__proto__` existiert nue als "Setter" Funktion, die durch die JavaScript Engine automatisch aufgerufen wird, wenn man versucht `__proto__` einen neuen Wert zuzuweisen. Der eigentliche Wert für `__proto__` wird in einer "versteckten Variable" gespeichert.
1089 |
1090 | Im Gegensatz dazu, wird für `obj1` durch `JSON.parse()` eine Property `__proto__` erzeugt (sozudagen keine "versteckte Variable"). Der "Setter" welcher die "versteckte" Variable belegt, wird dabei gar nicht aufgerufen. Der tatsächliche Wert für dieses Attribut bleibt damit unverändert und wird durch eine neuer Variable namens `__proto__` "überlagert".
1091 |
1092 | Dies spiegeln auch die erzeugten Objekte wieder.
1093 |
1094 | Das Objekt `obj1` besitzt eine sichtbare Property namens `__proto__`, es handelt sich dabei aber nicht um die eigentliche Prototypen-Definition, welche über Getter und Setter angesprochen werden müsste.
1095 |
1096 | ```
1097 | > obj1 = JSON.parse('{"__proto__": { "toString": "this is not a function" } }')
1098 | { __proto__: { toString: 'this is not a function' } }
1099 | ```
1100 |
1101 | Das Objekt `obj2` besitzt **keine** sichtbare Property namens `__proto__`. Die Prototypen-Definition ist zwar vorhanden, aber die Getter/Setter sind nicht sichtbar. Bei der Erzeugung von `obj2` wurde aber der Setter für `__proto__` genutzt, um die (unsichtbare) Prototypen-Definition zu überschreiben.
1102 |
1103 | Wenn ich vorher vom "Vorbereiten" einer "Prototype Pollution" gesrochen habe, meine ich trotzdem `obj1`. Auch wenn hier der Prototyp nicht überschrieben werden konnte, existiert hier eine Property namens `__proto__`, deren Wert geeignet wäre die `toString` Methode im Prototyp eines anderen Objektes zu überschreiben. In Pseudo-Code könnte dies etwa so aussehen:
1104 |
1105 | ```
1106 | targetObject.setPrototypeOf(obj1.__proto__)
1107 | ```
1108 |
1109 | Im Pseudo-Code Beispiel ist `obj1.__proto__` eine Property, die zwar nicht den eigentliche Prototypen-Definition abbildet (kein Getter/Setter), aber von `JSON.parse` mit einem Wert belegt wurde, der in einer Prototypen-Definition die Methode `toString` überschreiben würde. Für das Objekt `targetObject` wird der Setter für die Prototypen-Definition aufgerufen und daher die eigentliche Prototypen-Definition mit dem Wert der Property aus `obj1.__proto__` überschrieben.
1110 |
1111 | Ergebnis: Im Gegensatz zu dem Objekt `obj1`, wurde für das Objekt `targetObject` die `toString` Methode überschrieben, da der Prototyp unter Nutzung eines "Setters" mit einem Wert belegt wurde.
1112 |
1113 | Der zweite Schritt zu eine "Prototyp Pollution" ist also: Mit dem Wert der `__proto__` Property eines Objektes welches aus einem JSON String geparst wurde (der String wird vom Nutzer/Angreifer kontrolliert), die Prototypen-Definition eines weiteren Objektes zu überschreiben. Um einen Effekt zu erzielen, muss dazu der "Setter" des Zielobjektes benutzt werden, welche den Prototyp überschreibt.
1114 |
1115 | Im Pseudo-Code hieß dieser "Setter" `setPrototypeOf`. Der tatsächliche Aufrufername des "Setters" ist allerdings `__proto__`. Und hier wird es verwirrend, daher auch die ausführliche Abgrenzung zwischen "Setter" und "Property" die in einer "versteckten Variable" gespeichert wird.
1116 |
1117 | Zur Verdeutlichung, ein weitere Code-Schnipsel:
1118 |
1119 | ```
1120 | > obj1 = JSON.parse('{"__proto__": { "toString": "this is not a function" } }')
1121 | { __proto__: { toString: 'this is not a function' } }
1122 | > obj1.toString()
1123 | '[object Object]'
1124 | > obj1
1125 | { __proto__: { toString: 'this is not a function' } }
1126 |
1127 |
1128 | > targetObject = {}
1129 | {}
1130 | > targetObject.toString()
1131 | '[object Object]'
1132 | > targetObject.__proto__ = obj1.__proto__
1133 | { toString: 'this is not a function' }
1134 | > targetObject.toString()
1135 | Uncaught TypeError: targetObject.toString is not a function
1136 | >
1137 | ```
1138 |
1139 | Für `obj1` wird erneut mittels `JSON.parse` eine Property `__proto__` erzeugt, die geeignet ist den Prototypen eines Objektes zu überschreiben (konkret die `toString` Funktion). Für `obj1` findet das Überschreiben aber nicht statt (`obj1.toString()` funktioniert unverändert), weil der Prototyp nicht mittels eines "Setters" platziert wurde.
1140 |
1141 | In der Zeile `targetObject.__proto__ = obj1.__proto__` wird dann für das Objekt `targetObject` der **Setter** für `__proto__` angesprochen, um den Wert der Property `obj1.__proto__` zuzuweisen. In der Folge wird `targetObject.toString()` Methode so überschrieben, dass sie beim Aufruf einen Fehler erzeugt.
1142 |
1143 | Entscheidend:
1144 |
1145 | - `targetObject.__proto__` stellt eine Property dar, die (transparent) von einer Getter oder Setter Funktion angesprochen wird. Die Getter/Setter für `targetObject.__proto__` referenzieren dabei den tatsächlichen Prototyp des Objektes `targetObject`.
1146 | - `obj1.__proto__` stellt eine Property dar, die direkt - also ohne Getter/Setter - angesprochen wird. **Die Property bildet aber nicht den tatsächlichen Prototyp des Objektes**. Ursache hierfür ist die Objekt-Erzeugung durch `JSON.parse`, bei der nie Getter oder Setter für Eigenschaften aus dem JSON-String erzeugt oder angesprochen werden.
1147 | - Auch für den Prototyp von `obj1` existieren Getter und Setter, welche aufgrund der Namensgleichheit zu der neuen Property `__proto__` überlagert werden.
1148 |
1149 | Immer noch kompliziert?! Es wird nicht besser (JavaScript ist eine Sprache mit vielen Fallstricken):
1150 |
1151 | Es dürfte unwahrscheinlich sein, dass Entwickler "Prototype Pollution" fordern, indem Sie mutwillig erlauben, Prototypen in der zu überschreiben indem sie Code wie den folgenden platzieren:
1152 |
1153 | ```
1154 | targetObjectExploitMe = {}
1155 | targetObjectExploitMe.__proto__ =
1156 | ```
1157 |
1158 | Aber, man kann Objekt-Properties in JavaScript auch anders ansprechen:
1159 |
1160 | ```
1161 | targetObjectExploitMe = {}
1162 | targetObjectExploitMe["__proto__"] =
1163 | ```
1164 |
1165 | Der Code drückt genau das gleiche aus wie bisher, nur wird die Property `__proto__` im zweiten Fall nicht mehr in der "punktierten" Schreibweise angesprochen, sondern der Property-Name wird als String in eckigen Klammern übergeben ("property lookup"). Zunächst ist das nur eine formale Änderung der Syntax, aber sie hat einen Zweck. Im gegensatz zur punktierten Schreibweise, erlaubt sie Properties generisch Art und Weise anzusprechen, z.B. wenn der Name einer Property erst zur Laufzeit bekannt ist.
1166 | Gerne wird diese "property lookup" Schreibweise auch benutzt, um die (erst zur Laufzeit bekannten) Properties eines Objektes, ganz oder teilweise, in **gleichnamige** Properties eines anderen Objektes zu kopieren. **Existiert in dem Zielobjekt bereits eine Property des gewünschten Namens, wird dabei deren Wert überschrieben oder - wenn definiert - deren Setter aufgerufen**.
1167 |
1168 | Allen die den Ausführungen bisher folgen konnten, sollte nun ein Licht aufgehen:
1169 |
1170 | JavaScript Code, welcher...
1171 |
1172 | - Properties eines Objektes, ganz oder in Teilen in ein anderes Objekt kopiert
1173 | - dabei die Namen der Properties beibehält
1174 | - **ohne eine Eingabe-Filterung** für Properties vorzunehmen, welche zur Prototypenmanipulation genutzt werden können
1175 |
1176 | ... **ermöglicht "Prototype Pollution"-basierte Angriffe**, auch wenn die Input-Objekte mit `JSON.parse` erstellt wurden.
1177 |
1178 | Es gibt eine Vielzahl von Anwendungsfällen, in denen entsprechender Code entstehen kann (wenn keine Sicherheitsreviews oder -audits stattfinden).
1179 |
1180 | Die `escapeProblematicCharacters` aus dem Luca "Health-Department Frontend" der Version v1.1.11 gibt hierfür ein Musterbeispiel.
1181 |
1182 | ```
1183 | export function escapeProblematicCharacters(object) {
1184 | const target = {};
1185 | Object.entries(object).forEach(([key, value]) => {
1186 | if (typeof value === 'string') {
1187 | const valueWithReplacedPlus = value.replace('+', '00');
1188 | target[key] = valueWithReplacedPlus.replace(/^([=-@\t\r])/, "'$1");
1189 | } else {
1190 | target[key] = value;
1191 | }
1192 | });
1193 | return target;
1194 | }
1195 | ```
1196 |
1197 | 1. Die Funktion übernimmt ein beliebiges Objekt mit dem Namen `object` als Paraemeter (in der Praxis handelt es sich hierbei um ein objekt, welches mittels `JSON.parse()` aus Nutzer-kontrollierten Daten enstanden ist)
1198 | 2. Der Abschnitt `Object.entries(object).forEach(([key, value]) => { ... ...})` iteriert dabei über **alle** Properties des Eingangsobjektes und übergibt den Namen der jeweiligen Property in die Variable `key`, sowie den Wert der Property in die Variable `value`
1199 | 3. Für **alle** Properties von `object`, deren Wert nicht vom Typ `string` ist, wird durch den Code `target[key] = value;` im neuen Objekt `target` eine Property mit gleichem Namen angelegt und (ungefiltert) mit dem Wert aus der Variable Value belegt. **Zur Erinnerung: Existiert bereits eine Property mit diesem Namen, wird deren Wert entweder mittels Setter oder direkt überschrieben**.
1200 |
1201 | Zum Abrunden des Bildes, hier ein Beispiel: Die Funktion `escapeProblematicCharacters` vollendet die "Prototype Pollution" (überschreiben der Methode `toString` für `escapedObject2`), welche im Objekt `jsonParsedObject2` vorbereitet wurde. Das Objekt `jsonParsedObject2` wurde dazu mittels `JSON.parse()` aus einem (Nutzer-kontrollierten) String erzeugt, daher konnte die `toString` Methode zunächst nicht direkt überschrieben werden:
1202 |
1203 | ```
1204 | > jsonParsedObject1 = JSON.parse('{"foo": "bar"}')
1205 | { foo: 'bar' }
1206 | > jsonParsedObject2 = JSON.parse('{"foo": "bar", "__proto__": {"toString":"I am not a function"}}')
1207 | { foo: 'bar', __proto__: { toString: 'I am not a function' } }
1208 | > jsonParsedObject1.toString()
1209 | '[object Object]'
1210 | > jsonParsedObject2.toString()
1211 | '[object Object]'
1212 | > escapedObject1 = escapeProblematicCharacters(jsonParsedObject1)
1213 | { foo: 'bar' }
1214 | > escapedObject2 = escapeProblematicCharacters(jsonParsedObject2)
1215 | { foo: 'bar' }
1216 | > escapedObject1.toString()
1217 | '[object Object]'
1218 | > escapedObject2.toString()
1219 | Uncaught TypeError: escapedObject2.toString is not a function
1220 | >
1221 | ```
1222 |
1223 | Das Überschreiben der `toString` Methode mit ungültigen Daten, diente in den Ausführungen als Beispiel zur Objektmanipulation über "Prototype" Pollution. In der Praxis werden Objekte so manipuliert, dass **kontextabhängig** ein Effekt erzielt wird. Die Breite an möglichen Auswirkungen ist daher hoch und ebenfalls kontextabhängig (Denial-of-Service, Privilege Escalation, Remote-Code-Execution etc etc etc).
1224 |
1225 | Ich möchte an dieser Stelle daran erinnern, dass wir die Funktion `escapeProblematicCharacters` für einen Teil des Luca-Codes betrachtet haben, der sie innerhalb von der Funktion `filterTraceData` ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/utils/sanitizer.js#L32)) aufruft. Wie ausgeführt, findet beim Aufruf von `escapeProblematicCharacters` innerhalb von `filterTraceData` eine Vorfilterung der zulässigen Objekt-Properties statt (mit `loadsh.pick()` - [Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/utils/sanitizer.js#L34)). Diese Filterung entfernt zwar aufgrund des "white listing"-Ansatzes Properties wie `__proto__` aus dem Objekt welches an `escapeProblematicCharacters` übergeben wird, tut dies aber nicht zweckgebunden. Der Vorfilter bezieht sich semantisch auf den Inhalt eines `userData` Objektes, nicht auf die Unterdrückung von "Prototype Pollution". Eine Eingabe-Validierung zur Verhinderung von "Prototype Pollution" müsste stattdessen **innerhalb** der `escapeProblematicCharacters` Funktion platziert werden und Properties mit Bezug zu "Prototype Pollution" unterdrücken (es handelt sich hierbei um eine gut definierte und beschränkte Auswahl von Property-Namen, so dass sogar ein "Black Listing" Ansatz denkbar wäre). Dass eine solche Filterung innerhalb der `escapeProblematicCharacters` unterlassen wird, birgt eine hohe Gefahr Angriffsmöglichkeiten zu schaffen, sobald die Funktion an anderen stellen im Code verwendet wird (die eben kein `userData` Objekt mit gefilterten Properties als Parameter übergeben).
1226 |
1227 | Ein Beispiel in der die Funktion `escapeProblematicCharacters` mit einem **vollkommen ungefilterten** JavaScript Objekt - welches ebenfalls **mittels `JSON.parse()` aus Nutzer-kontrollierten Daten erzeugt** wurde - aufgerufen wird, findet sich hier: [Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.11/services/health-department/src/utils/decryption.js#L165). Verwertet werden in diesem Fall die schon erwähnten "additional trace data" Objekte (Vergleiche Abschnitt 1.2 und 1.2.1).
1228 |
1229 | Ein Proof-Of-Concept zur Ausnutzung dieser potentiellen "Prototype Pollution Schwachstelle" wurde nicht ausgearbeitet. **Allerdings zeigt die durchgängige mangelhafte oder überhaupt nicht vorhandene Filterung von Nutzerdaten erneut, dass zahlreiche Ansatzpunkte für ausnutzbare Schwachstellen im Luca-Code existieren (hier im speziellen für den System-Anteil der intern im Gesundheitsamt läuft).**
1230 |
1231 | ---
1232 |
1233 | Nach diesem, doch ausfühlichen, Exkurs zu einem anderen Code-Injection-Szenario, möchte ich zur "CSV Injection" zurück kommen. Offen war noch, wie die `escapeProblematicCharacters` dieser Problemstellung begegnet. Da die Funktion bereits mehrfach betrachtet wurde, kann man dies kurz fassen:
1234 |
1235 | Für jede Property des \*\*Datentypes `string` werden reguläre Ausdrücke angewendet, die:
1236 |
1237 | - das erste Zeichen des Property-Wertes durch `"00"` ersetzen, sofern es ein `"+"` war
1238 | - dem ersten Zeichen des Property-Wertes ein `'` voranstellen, sofern es ein `=`, `-`, `@`, `\t` oder `\r` war
1239 |
1240 | ```
1241 | if (typeof value === 'string') {
1242 | const valueWithReplacedPlus = value.replace('+', '00');
1243 | target[key] = valueWithReplacedPlus.replace(/^([=-@\t\r])/, "'$1");
1244 | }
1245 | ```
1246 |
1247 | Dass dieser "Filter" keinen ausreichenden Effekt entfalten kann (bezüglich der OWASP Empfehlungen zu CSV-Injection), wird auf den ersten Blick klar:
1248 |
1249 | - der Filter operiert nicht auf Eingabedaten, die einen CSV-Zelleninhalt repräsentieren, sondern auf Eigenschaften von JavaScript-Objekten, welche später von einer externen Library weiterverarbeitet werden
1250 | - der Filter unternimmt keine Anstrengungen weitere Zeichen zu adressieren, die im Ausgabe-Kontext der Zielbibliothek ebenfalls eine Bedeutung haben könnten
1251 |
1252 | Der Filter beantwortet also folgende Fragen für seinen Zielkontext nicht (Zielkontext ist generieren von Eingabedaten für die `react-csv` Bibliothek):
1253 |
1254 | - Übernimmt die Zielbibliothek die Ausgbaekodierung der Daten gemäß RFC4180 oder muss diese hier erfolgen?
1255 | - Betrachtet die Zielbibliothek die Daten überhaupt als Zelleninhalte oder nur als reinen Text der nicht weiter kodiert wird?
1256 | - Beinhaltet das Filter-Objekt Properties, die im Kontext der Zielbibliothek als "Steuerparameter" betrachtet werden (z.B. Optionen um den Zellentrenner zu ändern usw)?
1257 |
1258 | Die Ausführlichkeit dieses Kapitels, soll nicht darüber hinwegtäuschen, dass ein "Threat Actor" sehr schnell zu dem Schluss kommt, dass der Code Angriffe durch CSV-Injection möglich sind. Mit Blick auf den Filter-Code werden ad-hoc Eingabedaten ersichtlich, die diesen unverändert durchlaufen und in der Weiterverarbeitung zu einer ausnutzbaren Schwachstelle führen können.
1259 |
1260 | Einige Beispiele (betrachtet wird nur die `userData.fn` Property, also der Vorname aus den Kontaktdaten, prinzipiell kann aber jedes Attribut der Kontaktdaten genutzt werden):
1261 |
1262 | ```
1263 | userData.fn = '"=Formel()"'
1264 | /*
1265 | * Der String beginnt mit dem Zeichen '"' welches der Filter zunächst zulässt.
1266 | * Zeichen nach dem ersten Zeichen werden vom Filter nicht mehr betrachtet.
1267 | * Wenn die externe `react-csv` Bibliothek den String als CSV-Zelle interpretiert
1268 | * (und nicht als Zelleninhalt,welcher der noch zu escapen ist), findet sich die
1269 | * Zelle unverändert in der finalen CSV-Ausgabe wieder.
1270 | * Da die die umschließenden Double Quotes nicht zum Zelleninhalt gehören, sondern
1271 | * ein Feld mit Inhalt des Typs 'escaped' darstellen (vergleiche RFC4180),
1272 | * interpretieren Tabellenkalkulationen die Zelle RFC-Konform als '=Formel()' -
1273 | * also als Formel, welche zur Darstellung der Zelle zu berechnen ist.
1274 | *
1275 | * Würde die `react-csv` Bibliothe den Inhalt aber als Feld des Types `non-escaped`
1276 | * ansehen (vergl RFC4180), müsste der Inhalt innerhalb von react-csv noch
1277 | * RFC-konform zu einer Zelle kodiert werden. Das Ergebnis wäre:
1278 | * """=Formel()""" (Umwandeln von DQUOTES zu 2DQUOTES, Umschließen mit DQUOTES)
1279 | */
1280 |
1281 | userData.fn = 'Hans",=Formel(),"'
1282 | /*
1283 | * Der String geht ebenfalls davon aus, das nicht klar ist wie Zellen in der
1284 | * Weiterverarbeitung kodiert werden und beginnt erneut mit einem erlaubten Zeichen.
1285 | * Hier wird allerdings versucht eine bestehende zelle zu schließen und eine neue
1286 | * Zelle einzufügen. In der vorgesehenen Zelle der finalen CSV-Ausgabe, würde dann
1287 | * noch der Name "Hans" stehen, gefolgt von einer Zelle mit einer Formel. Die
1288 | * Annahme ist hier, dass alle Zelleninhalte mit DQUOTES umschlossen werden.
1289 | * Weiter wird von ',' als zellentrenner ausgegangen.
1290 | */
1291 |
1292 | userData.fn = 'Hans\r\n=Formel()\r\n"'
1293 | /*
1294 | * Analog zum vorherigen Beispiel wird eine neue Zelle eingefügt.
1295 | * Stat Zellentrennern werden hier Zeilentrenner verwendet.
1296 | * Diese wären unabhängig vom gewählten Zellentrenner (welcher statt
1297 | * eines Kommas auch ein Semikolon oder jedes andere zeichen sein könnte)
1298 | */
1299 | ```
1300 |
1301 | Es wäre mindestens zu betrachten Wie die Daten in der Weiterverarbeitung durch `react-csv` kodiert werden, um diese innerhalb `escapeProblematicCharacters` für die Weiterverarbeitung vorzubereiten (Output-Encoding). Aber, selbst wenn man diese Betrachtungen vornehmen würde, kann der Filter nie robust arbeiten. Um robust zu arbeiten, müsste er **innerhalb** der `react-csv` Library platziert werden (wurde bereits ausführlich erläutert).
1302 |
1303 | Zusätzlich wurde auf Entwickler-Seite unterlassen zu analysieren, wie die Daten in der Weiterverarbeitung von `react-csv` kodiert werden.
1304 |
1305 | Ein Akteur der Sicherheitslücken, mit einem so eng definierten Scope (ausschließlich "CSV Injection"), sucht, wird diese Fragestellungen für sich beantworten ... und er wird es schnell tun.
1306 |
1307 | Hierbei würde er folgendes feststellen:
1308 |
1309 | Die genutzte `CSVLink` Komponente der `react-csv` Library übernimmnt Eingabedaten als zweidimensionales Array, um Zeilen und Spalten zu repräsentieren. In etwa so:
1310 |
1311 | ```
1312 | tableData = [
1313 | [valueZeile1Spalte1, valueZeile1Spalte2],
1314 | [valueZeile2Spalte1, valueZeile2Spalte2],
1315 | ]
1316 | ```
1317 |
1318 | Im Rahmen von Tests wären folgende Fragen zu beantworten:
1319 |
1320 | 1. Wie geht die `CSVLink` Komponente mit verschiedenen Datentypen um?
1321 | 2. Welche Teile der RFC4180 konformen Ausgabekodierung übernimmt die `CSVLink` Komponente? (Daraus abgeleitet: Welcher Teil der RFC4180 konformen Kodierung ist durch den Luca-Code zu übernehmen?)
1322 |
1323 | **Vor Beantwortung der Fragen, erneut der Hinweis: Der Luca-Code hat nicht die Kontrolle über die Verarbeitung der Daten durch die `react-csv` Library. Für jede Versionsänderung dieser Library (oder einer ihrer Dependencies), wären diese Fragen neu zu beantworten und der Luca-Code entsprechend anzupassen. Auch wiederhole ich gerne, dass es hier nicht nur um "nicht vertrauenswürdigen" - aber ungetesteten - Code aus einer externen Quelle geht, sondern um Code der letztendlich auf Fachsystemen in einem Gesundheitsamt zur Ausführung kommt.**
1324 |
1325 | Für einige ausgewählte Beispiele an Eingabedaten verhält sich die `CSVLink` Komponente wie folgt:
1326 |
1327 | (Eingabedaten jeweils für `Download CSV`)
1328 |
1329 | ### Beispiel 1
1330 |
1331 | Input für die `data` property von `CSVLink`:
1332 |
1333 | ```
1334 | input = [
1335 | [{"I": "am an object"}, "i am a string", 1337, -1e500] // Tabellenzeile mit 4 verschiedenen Datentypen
1336 | ]
1337 | ```
1338 |
1339 | Erzeugte CSV-Ausgabe:
1340 |
1341 | ```
1342 | "[object Object]","i am a string","1337","-Infinity"
1343 | ```
1344 |
1345 | Feststellung:
1346 |
1347 | `CSVLink` akzeptiert verschiedene JavaScript Datentüben als Werte für spätere Tabellenzellen. Jeder Wert wird in eine `string`-Repräsentation transformiert, für Objekte wird die `toString()` Methode genutzt.
1348 | **In der CSV-Repräsentation werden alle Werte mit DQOUTEs (`"`) umschlossen. Gemäß RFC4180 bedeutet dies, dass die Zellinhalte vom Typ `encoded` sind, daher kann in der Weiterverarbeitung davon ausgegangen werden, dass für nicht zulässig Zeichen ein escaping stattgefunden hat** (vergleiche Abschnitt 3.6.1.1).
1349 |
1350 | ---
1351 |
1352 | ### Beispiel 2
1353 |
1354 | Input für die `data` property von `CSVLink`:
1355 |
1356 | ```
1357 | input = [
1358 | [["i","am","an","array"]], // Tabellenzeile mit Feld des Typs Array
1359 | [["nested",["inner","array"]]], // Tabellenzeile mit geschachteltem Array als Feld
1360 | ]
1361 | ```
1362 |
1363 | Erzeugte CSV-Ausgabe:
1364 |
1365 | ```
1366 | "i,am,an,array"
1367 | "nested,inner,array"
1368 | ```
1369 |
1370 | Feststellung:
1371 |
1372 | Für JavaScript Arrays werden die Einträge zunächst in Strings transformiert (analog zu Beispiel 1) und anschließend zu einem Kommaseparierten String zusammengeführt ("join"). Das Ergebnis wird erneut mit DQUOTEs umschlossen (impliziert `encoded` Zelleninhalt gem. RFC4180).
1373 |
1374 | ---
1375 |
1376 | ### Beispiel 3
1377 |
1378 | Input für die `data` property von `CSVLink`:
1379 |
1380 | ```
1381 | // Tabellenzeile mit EINEM welches Zeichen enthält die nach RFC4180 zu kodieren sind
1382 | input = [
1383 | ["eine,zelle,mit \"encoded\" chars"]
1384 | ]
1385 | ```
1386 |
1387 | Erzeugte CSV-Ausgabe:
1388 |
1389 | ```
1390 | "eine,zelle,mit "encoded" chars"
1391 | ```
1392 |
1393 | Feststellung:
1394 |
1395 | Das Ergebnis wird erneut mit DQUOTEs umschlossen, allerdings wird die implizierte Kodierung nach RFC4180 nicht von `react-csv` übernommen. Die zu erwartende Ausgabe gem. RFC4180 wäre `"eine,zelle,mit ""encoded"" chars"`. Im speziellen wird hier kein "escaping" von DQUOTES (`"`) zu 2DQUOTES (`""`) vorgenommen. Die wäre demnach vom Luca-Code, im Rahmen der Ausgabe-Kodierung vür die `CSVLink` Komponente, sicherzustellen.
1396 |
1397 | ---
1398 |
1399 | Die ausnutzbare Schwachstelle wird damit Augenscheinlich. Ein `userData` Objekt der Form ...
1400 |
1401 | ```
1402 | {
1403 | "fn": "Max",
1404 | "ln": "Mustermann\", \"=Formel(param1, param2)\", \"",
1405 | "pn": "0017612345678",
1406 | ...
1407 | }
1408 | ```
1409 |
1410 | ... würde, mangels Filterung der Daten für den Ausgabe-Kontext `CSVLink` zu folgenden Eingabe-Daten ...
1411 |
1412 | ```
1413 | input = [
1414 | ["Max", "Mustermann\", \"=Formel(param1, param2)\", \"", "0017612345678", ...],
1415 | ...
1416 | ]
1417 | ```
1418 |
1419 | ... und nach der Weiterverarbeitung durch die `CSVLink`Komponente zu folgenden CSV-Daten:
1420 |
1421 | ```
1422 | "Max", "Musterman", "=Formel(param1, param2)", "", "0017612345678"
1423 | ```
1424 |
1425 | Diese CSV-Daten würden beim Import in eine Tabellenkalkulation folgerichtig die Berechnung der Formel `=Formel(param1, param2)` zur Folge haben (wie in den OWASP Empfehlungen erläutert, die umschließenden DQUOTEs werden dabei gem. RFC4180 nich als Bestandteil des Zelleninhaltes interpretiert).
1426 |
1427 | Dies unterstreicht einmal mehr, dass der `escapeProblematicCharacters` logisch falsch positioniert ist, den er müsste einen Anteil der RFC4180-Kodierung übernehmen (Escaping von `"` zu `""`), während die nachgeordnete React Komponente `CSVLink` den zweiten Teil der Kodierung übernimmt (Umschließen des kodierten Inhaltes mit DQUOTES). Diese beiden Aspekte der RFC4180-Kodierung sind aber untrennbar (atomar) und können gar nicht über verscheidene Software-Produkte verteilt werden (ob ein "Enclosing" mit DQUOTEs vorzunehmen ist, hängt davon ab, ob im Zelleninhalt überhaupt Zeichen vorkommen die zu "escapen" sind).
1428 |
1429 | Vor allem aber zeigt sich hier nicht nur überdeutlisch, wie gefährlich es ist Sicherheits-kritische Funtionalitäöt an (nicht vertrauenswürdigen) externen Code auszulagern, sondern dass **KEINE Ressourcen in "Testing" investiert** wurden. Dies trifft nicht nur auf den "Spezialfall CSV Injection" zu, sondern auf die gesammte Verarbeitungskette nicht vertrauenswürdiger Daten, welche im Luca-System von externen Teilnehmern auflaufen.
1430 |
1431 | Aus der Perspektive eines "Threat Actors" sind solche Tests allerdings sehr schnell durchführbar, denn es kann gezielt auf eine relevante Auswahl möglicher Angriffsvektoren getestet werden. Im Gegensatz dazu muss auf Entwicklerseite **jedes** Angriffszenario mit Tests abgedeckt, um mit "Sicherheit" werben zu können. Das Luca-System unterlässt solche Tests weitestgehend, was sich auch in der Programmier-technischen Umsetzung stark wiederspiegelt.
1432 |
1433 | Für das Szenario "CSV Injection" betrug die Zeitspanne vom ersten Blick in den `escapeProblematicCharacters` Code bis zum Test und der Bestätigung einer ausnutzbaren "CSV Injection"-Schwachstelle etwa 10 Minuten (Referenzsystem stand bereits bereit, ebenso konnten Luca-Nutzer mit beliebigen Daten bereits automatisiert zu registrieren). Der Aufwand der auf der anderen Seite betrieben werden muss, um das Risiko von "Code Injection" Angriffen zu minimieren, lässt sich anhand des Umfangs der hier gemachten Ausführungen erahnen (obwohl sie nur einen "Datenstrom" betrachten).
1434 |
1435 | Auch ein durchschnittlich befähigter Angreifer dürfte, angesichts der vielzahl Mängel im Code, sehr schnell zu verwertbaren Ergebnissen kommen. Die Komplexität und "Verwinkelung" des Codes erschwert aber die Analyse von Ursache-Wirk-Ketten für die Entwickler immens. Ein Problem wie Code Injection als "behoben" oder "ausgeschlossen" anzusehen, ist mit dem aktuellen Code-Design nahezu unmöglich.
1436 |
1437 | ## 3.6.1.1 Updates zur Mitigation von "CSV Injection" (bis `v1.2.1`)
1438 |
1439 | Nachdem der Hersteller (auch öffentlich) damit konfrontiert, dass das Luca-System Angriffe durch Nutzer auf angebundene Gesundheitsämter über den "CSV Injection"-Vektor nicht verhindert. Dies wurde unter Verweis auf die Umsetzung der OWASP Empfehlungen negiert (vergleiche Abschnitt 3.3).
1440 |
1441 | Dass derartige Aussagen durch den Luca-Hersteller auch getroffen werden, ohne dass die entsprechenden Funktionalitäten getestet werden, war zu diesem Zeitpunkt schon offensichtlich.
1442 |
1443 | Vor dem Hintergund des "Responsible Disclosure" Prozesses, musste man sich an dieser Stelle Fragen, wie man, vor diesem Hintergrund, mit dem Wissen um eine ausnutzbare Sicherheitslücke umgeht, der laut Hersteller bereits mit den richtigen Maßnahmen begegnet wird.
1444 |
1445 | Dem "üblichen Prozedere" folgend, hätte man hierzu ein Proof-of-Concept (PoC) erstellen und dem hersteller bereitstellen können, welches **ein oder zwei** konkrete Beispiele zur ausnutzung der Schwachstelle liefert. Ein solches PoC könnte aber in keinem Fall die gesammte Kette an Fehlern in der Ein- und Ausgabefilterung abbilden, oder gar weitere Systemkomponenten abdecken. Vor allem aber, wäre nicht zu erwarten gewesen, dass der Hersteller die Problemstellungen ganzheitlich betrachtet (gesammte Datenverarbeitungskette) oder gar hinreichende Funktions- und Sicherheitstests etabliert.
1446 |
1447 | Entlang der Entfahrungen aus der Vergangenheit, wäre bei diesem Hersteller eher zu erwarten, dass eine sehr spezifische Möglichkeit einen Angriff durchzuführen, mittels Patch unterbunden wird, ohne das 'Problem zu analysieren oder die "Lösung" umfassend zu testen. Umfasssende Tests müssten hier unweigerlich zur Neugestaltung größerer Systemanteile führen. Auch dieser Aspekt wäre - in Art und Umfang - weder über einem PoC noch über ein Advisory geeignet an den Hersteller zu kommunizieren gewesen.
1448 |
1449 | Demgegenüber stand das hohe Sicherheitsrisiko, dem die Gesundheitsämter über 3 Wochen seit dem öffentlichen Bekanntwerden der Code-Injection-Anfälligkeiten ausgesetzt waren.
1450 |
1451 | Die Bereitstellung eines PoC an den Hersteller hätte also dem **verantwortungsbewussten** Umgang mit bekannten Sicherheitslücken wiedersprochen, insbesondere mit Blick auf den "Responsible Disclosure" Prozess. Ebenso, schloss sich die Veröffentlichung von nutzbaren Schadcode aus.
1452 |
1453 | Um das Problem also in das Bewusstsein zu rücken und zu erzwingen, dass die beworbene Funktionalität zur Verhinderung von "CSV Injections" durch den Hersteller getestet und **funktional** gestaltet wird, wurde die Durchführbarkeit eines Angriffes exemplarisch demonstriert ([Video Link](https://vimeo.com/558459255)). Obwohl die Schwachstelle bereits öffentlcih bekannt war, wurde hier großer Wert drauf gelegt, dass kein funktionierender Schadcode bekannt wird.
1454 |
1455 | Die Erwartung, dass der Hersteller nun Tests ansetzt, welche Fehler in der Ein- und Ausgbaeverarbeitung von nicht vertrauenswürdigen Daten im Luca-System aufdecken, um diese dann abzustellen (verifiziert durch weitere Tests) wurde enttäuscht:
1456 |
1457 | Etwa **2 Stunden nach Veröffentlichung** der Video-Demonstration wurde ein Patch veröffentlicht ([Link](https://gitlab.com/lucaapp/web/-/commit/d671d978f59507b8b25ea46381133eb249df111c)) und damit einhergehend eine Pressemitteilung. In der Mitteilung wurde kund gegeben, das man `"...heute von einem möglichen Missbrauch im Zusammenhang mit der Verwendung von luca und Microsoft Excel erfahren..."` habe und `"...böswillige Angreifer:innen diese Excel Lücke nicht mehr ausnutzen können..."`.!
1458 |
1459 | Ich orientiere mich vorzugsweise an technisch belegbaren Fakten (Code), aber folgendes ist auch ein Fakt:
1460 |
1461 | Es existiert **keine seriöse Firma**, die:
1462 |
1463 | - Sicherheits- und Privacykritische Software entwickelt
1464 | - eine Sicherheitslücke von der sie **"heute"** erfahren hat
1465 | - welche, der Einschätzung nach, von einem nachgeordneten System mitverantwortet wird ("Excel")
1466 | - innerhalb von **2 Stunden**:
1467 | 1. Bewertet (serverity)
1468 | 2. Die Existenz der Lücke bestätigt (acknowledgement)
1469 | 3. Ursachen vollumfänglich analysiert (investigate, auch mittels "Testing")
1470 | 4. Einen Patch entwickelt
1471 | 5. Dessen "Rollout" priorisiert (gem. impact)
1472 | 6. Den Patch in einer Referenzumgebung ausrollt (fix)
1473 | 7. "Nebenwirkungen" und Wirksamkeit des Patches überprüft (Entwicklung ergänzender Tests)
1474 | 8. Den Patch anschließend koodiniert im Produktionssystem ausrollt (deployment)
1475 | 9. die Wirksamkeit erneut testet und validieren läst
1476 | 10. ... und **dann** mitteilt, dass das Sicherheitsproblem mitigiert wurde
1477 |
1478 | Das nun bereitgestellte Update ([v1.1.12 - Link](https://gitlab.com/lucaapp/web/-/commit/d671d978f59507b8b25ea46381133eb249df111c)), griff zwar CSV-Injection auf, zeigte aber klar:
1479 |
1480 | - die spezifischer Problemstellung (CSV Injection) wurde nicht ausreichend durchdrungen
1481 | - die grundsätzliche Problemstellung (umfassende Filterung nicht vertrauenswürdiger Daten, **überall**), wurde nicht einmal betrachtet
1482 | - es erfolgen keine **aussagekräftigen** Tests
1483 |
1484 | Folgende Anpassungen wurden vorgenommen:
1485 |
1486 | ### Vorgenommene Anpassungen im ersten Patch (`v1.1.12`)
1487 |
1488 | Zunächst einmal wurde die `filterTraceData` durch eine Funktion `sanitizeForCSV` ersetzt ([Code Link](https://gitlab.com/lucaapp/web/-/commit/d671d978f59507b8b25ea46381133eb249df111c#76faa0dbfb936517acc6e375a4fa6a2f0be5c1be_19_3)).
1489 |
1490 | Hier zeigt sich bereits in der Namensgebung, warum Probleme entstehen, wenn man Eingabefilterung (Validierung nach Eingabekontext) und Ausgabefilterung (Kodierung für Ausgabekontext) nicht voneinander trennt.
1491 |
1492 | Die Funktion `filterTraceData` hatte dem Namen nach noch die Aufgabe, Arrays, bestehend aus entschlüsselten `Traces` zu filtern. Jeder Trace enthält dabei ein nicht vertrauenswürdiges JavaScript Objekt `userData`, welches von Nutzern beliebig gestaltet werden kann (da keine weiteren Filter vorgeschaltet sind).
1493 |
1494 | Bei der Funktion `sanitizeForCSV` handelt es sich dem Namen nach allerdings um einen Ausgabefilter, für den Zielkontext "CSV". **Spoiler**: Der tatsächliche Zielkontext ist noch immer eine externe ReactJS Library, für die keine Tests veröffentlicht wurden.
1495 |
1496 | Die beiden Filter müssten also ergänzend existieren, um wenigstens für einen "Verarbeitungszweig" von nicht Vertrauenswürdigen Daten, eine adäquate Filterung anzustreben. Hier wird ersetzt aber ein Filter den anderen.
1497 |
1498 | Wenig überraschend, entfällt damit auch die - ohnehin nur rudimentäre - semantische Filterung von `userData` Objekten, welche sichergestellt hat, dass das JavaScript Objekt nur "erwünschte" Properties enthält ([Code Link](https://gitlab.com/lucaapp/web/-/commit/d671d978f59507b8b25ea46381133eb249df111c#76faa0dbfb936517acc6e375a4fa6a2f0be5c1be_20_25)).Nun gut, diese, eigentlich zentral anzusetzende, Filterung wurde nur in einigen wenige Ausgabekontexten angesetzt. Mit dem Update existiert sie überhaupt nicht mehr.
1499 |
1500 | Ihrem Namen entsprechend wird die neue Funktion `sanitizeForCSV` nicht mehr zentral angewendet (auf Arrays von `traces` die `userData` Objekte enthalten), sonder dort wo man vermeintlich CSV-Ausgabedaten produziert: Nämlich für jeden Wert, der von `react-csv` später in eine CSV-Tabellenzelle transformiert werden soll ([Code-Beispiel 1](https://gitlab.com/lucaapp/web/-/blob/v1.1.12/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.helper.js#L232), [Code-Beispiel 2](https://gitlab.com/lucaapp/web/-/blob/v1.1.12/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.helper.js#L436)).
1501 |
1502 | Die nun ergänzten Tests für diesen Spezialfall ([Code Link](https://gitlab.com/lucaapp/web/-/blob/d671d978f59507b8b25ea46381133eb249df111c/services/health-department/src/utils/sanitizer.test.js)), sind nicht nur äußerst spartanisch, sie unterstreichen eines zusätzlich: Man hat weder "CSV Injection" als Problem durchdrungen, noch ist man sich über den Datenfluss im eigenen Software-Projekt im klaren, denn: **Ob CSV-Daten erzeugt die den OWASP Empfehlungen gerecht werden, muss an der CSV-Ausgabe getestet werden. Konkret an den Ergebnissen, welche die `CSVLink` Komponente der `react-csv` Library ausgibt.** Hier wird aber ausschließlich die neue `sanitizeObject` getestet (welche intern die `sanitizeForCSV` Funktion verwendet). Auch ist die Testabdeckung so reduziert, dass nichteinmal die wenigen Zeichen abgedeckt sind, die gemäß der OWASP-Empfehlungen al problematisch für CSV-Ausgaben gelten. So fehlt zum Beispiel das `@` in den Tests.
1503 |
1504 | Was man nur noch als "schlechten Witz" bezeichnen kann: **Die `sanitizeObject` Funktion, welche Gegenstand der Tests ist, kommt im Code außerhalb der Tests gar nicht zum Einsatz ([Gitlab Link](https://gitlab.com/search?search=sanitizeObject&group_id=11548988&project_id=25881780&scope=&search_code=true&snippets=false&repository_ref=v1.1.12&nav_source=navbar))!**
1505 |
1506 | Schaut man sich nun die Funktion `sanitizeForCSV` an, wird der Nachteil von "Sanitization" gegenüber "Ein- und Ausgabefilterung" nochmals deutlich. Das in den "Tests" unterschlagene Zeichen `@` gibt hier ein sehr gutes Beispiel. Zunächst aber der Quellcode der `sanitizeForCSV` Funktion ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.12/services/health-department/src/utils/sanitizer.js#L3)):
1507 |
1508 | ```
1509 | export const sanitizeForCSV = value => {
1510 | if (
1511 | typeof value === 'number' ||
1512 | typeof value === 'undefined' ||
1513 | typeof value === 'boolean' ||
1514 | value === null
1515 | )
1516 | return value;
1517 | if (typeof value === 'object') return mapValues(value, sanitizeForCSV);
1518 | const sanitizedString = value
1519 | .replaceAll(
1520 | /[^0-9A-Za-zçæœŒßäëïöüÿãñõâêîôûáéíóúýàèìòùÄËÏÖÜŸÃÑÕÂÊÎÔÛÁÉÍÓÚÝÀÈÌÒÙÇÆŒŒ]+/gi,
1521 | ' '
1522 | )
1523 | .replaceAll(
1524 | /DROP|DELETE|SELECT|INSERT|UPDATE|TRUNCATE|FROM|JOIN|CREATE/gi,
1525 | ' '
1526 | )
1527 | .replaceAll(/[\n\r]/g, ' ')
1528 | .replaceAll('"', '""')
1529 | .replace(/^\+/, "'+");
1530 |
1531 | const forbiddenLeadingSigns = ['=', '-', '@', '\t'];
1532 |
1533 | return forbiddenLeadingSigns.includes(sanitizedString?.charAt(0))
1534 | ? sanitizeForCSV(sanitizedString.slice(1))
1535 | : sanitizedString;
1536 | };
1537 |
1538 | export const sanitizeObject = object => mapValues(object, sanitizeForCSV)
1539 | ```
1540 |
1541 | Der Code der `sanitizeObject` Funktion wurde ebenfalls eingeblendet, um aufzuzeigen, dass es sich hier einmal mehr um eine redundante Funktionalität handelt, denn diese Funktion greift auf `sanitizeForCSV` zurück, obwohl `sanitizeForCSV` genau den gleichen Code imlementiert:
1542 |
1543 | ```
1544 | if (typeof value === 'object') return mapValues(value, sanitizeForCSV);
1545 | ```
1546 |
1547 | Bei der obigen Codezeile, handelt es sich um einen rekursiven Aufruf der `sanitizeForCSV` Funktion, für alle Properties eines Eingabe-Objektes. Da die Eingabe Objekte vom Nutzer beliebig gestaltbar sind (zur Erinnerung: die Funktion wird auf Properties des nicht-vertrauenswürdigen `userData` Objektes angewendet), können die Eingabedaten beliebig Tief verschachtelte Objekte enthalten. Laufzeitfehler bei der rekursiven Abarbeitung sind also "vorprogrammiert".
1548 |
1549 | Der Umstand, dass hier überhaupt andere Typen als `number` und `string` zulässig sind, zeugt von der groben Missachtung des Kontextes in welchem der Filter Eingesetzt wird: Als (valide) Eingaben kommen nur diese Datentypen in Betracht, valide Ausgaben sind immer vom Typ `string` (zur Weitergabe an `react-csv`). Da an anderer Stelle keine Eingabevalidierung mehr erfolgt müsste diese allerdings hier vorgenommen werden.
1550 |
1551 | Das "Durchreichen" von verschachtelten Objekten oder Objekten überhaupt ist nicht nur im tatsächlichen Ausgabekontext des externen React Moduls problematisch (Stichwort "Client Side Template Injection"). Die Funktion kommt auch vollkommen Kontextfremd zum Einsatz, Beispielsweise bei der Ausgabefilterung für Excel-Exporte welche **nicht im CSV-Format** erfolgen ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.1.12/services/health-department/src/components/App/modals/HistoryModal/ContactPersonView/ContactPersonView.helper.js#L65)).
1552 |
1553 | Die tatsächlichen Filteregelungen, basierend auf regulären Ausdrücken, kommen nur für Werte des Typs `string` zur Anwendung. Gewählt wird hier ein "Whitelisting Ansatz", (nur folgende Zeichen sind zulässig (als regulärer Ausdruck beschrieben):
1554 |
1555 | ```
1556 | [0-9A-Za-zçæœŒßäëïöüÿãñõâêîôûáéíóúýàèìòùÄËÏÖÜŸÃÑÕÂÊÎÔÛÁÉÍÓÚÝÀÈÌÒÙÇÆŒŒ]
1557 | ```
1558 |
1559 | Wie angekündigt wird hier das Problem eines Sanitizers offensichtlich: Das `@` ist für Eingabedaten, wie Email-Adressen, erwünscht. Im CSV-Kontext ist das `@` aber für **Zelleninhalte** verboten. Mit Ein- und Ausgabefilterung an den richtigen Stellen, wäre dies unproblematisch zu handhaben (vergleiche Abschnit 3.6.1.1 - Pseudocode Beispiel). Ein Sanitizer, der generisch arbeitet, muss das Zeichen `@` unterdrücken und schränkt damit mögliche Eingaben unnötig ein, obwohl sie valide wären. Eine Email-Adresse würde diesen Filter nicht mehr in verwertbarer Form durchlaufen.
1560 |
1561 | Insbesondere aber, habe ich angeführt, dass Sanitizer nur schwierig korrekt implementiert werrden können (aufgrund des generischen Ansatzes). Auch dieser Sanitizer versagt bezogen auf die Filterung für CSV Injections vollkommen. Für Eingabe-Objekte werden zwar die Property-Values gefiltert, nicht aber die Property-Keys (welche im finalen CSV-Output ebenfalls zum Tragen kommen, z.B. für "additional trace data").
1562 |
1563 | Ein Beispiel (das Zeichen `=` wird für Property-Keys nicht gefiltert):
1564 |
1565 | ```
1566 | > sanitizeForCSV({"=UnfilteredFormula()":"=FilteredFormula()"})
1567 | { '=UnfilteredFormula()': ' FilteredFormula ' }
1568 | ```
1569 |
1570 | **Der Filter ist damit absolut wirkungslos, wurde aber dennoch - ohne jegliche Tests - in verschiedensten öffentlichen Mitteilungen als funktionierende Lösung "verkauft".**
1571 |
1572 | Auf eine weitere Demonstration, der nach wie vor bestehenden Angreifbarkeit des Systems über CSV-Injections wurde verzichtet! Das Etablieren hinreichender Tests wären hier klare Augabe des Herstellers.
1573 |
1574 | Die Unterdrückung vollkommen CSV-Kontextfremder Zeichenketten, bedarf kaum der weiteren Kommentierung, ist aber ebenfalls Zeugnis des mangelndem Problembewusstseins. Folgende Zeichenketten wurden (case-insensitive) unterdrückt:
1575 |
1576 | ```
1577 | /DROP|DELETE|SELECT|INSERT|UPDATE|TRUNCATE|FROM|JOIN|CREATE/gi
1578 | ```
1579 |
1580 | Für Menschen die "Frommann" heißen, würde der Nachteil von Sanitizern ebenfalls schnell klar:
1581 |
1582 | ```
1583 | > sanitizeForCSV({"fn": "Max", "ln": "Frommann"})
1584 | { fn: 'Max', ln: ' mann' }
1585 | ```
1586 |
1587 | Zusammenfassen kann man den Patch wie folgt:
1588 |
1589 | - Eingabefilter wurden ersatzlos entfernt (semantische Validierung von `userData` an einigen Stellen)
1590 | - der Neue Ausgabe-Filter verfehlt seine Wirkung (keine effektive Unterdrückung von CSV Injection)
1591 | - bisherige Filter waren nicht an allen relevanten Stellen platziert - die neuen Filter sind dies auch nicht, werden aber zusätzlich im falschen Kontext verwendet (`SanitizeForCSV` für Excel Exporte (`xlsx`))
1592 |
1593 | **Der Abschnitt wurde nicht abgeschlossen. Es fehlen die Betrachtungen der Fehler bis v1.2.2**
1594 |
1595 | ## 3.6.x SORMAS CSV-Export
1596 |
1597 | _Abschnitt wurde nicht mehr begonnen_
1598 |
1599 | Kurzzusammenfassung:
1600 |
1601 | - alle Problemstellungen analog zu CSV Export
1602 | - Filter greifen nicht
1603 | - für derzeit aktuellste Version des Backend Codes (`v1.2.2`) ist CSVB Injection noch immer möglich
1604 |
1605 | ## 3.6.x Excel Export
1606 |
1607 | _Abschnitt wurde nicht mehr begonnen_
1608 |
1609 | - Export nutzt externe Library `react-data-export` in Version `0.6.0`
1610 | - Angreifbarkeit wahrscheinlich
1611 | - platzierte "Filterung" ist für CSV-Kontext vorgesehen, nicht XLSX
1612 |
1613 | ## 3.6.x WEB-UI Rendering
1614 |
1615 | _Abschnitt wurde nicht mehr begonnen_
1616 |
1617 | - keine Filterung
1618 | - potentiel Template Injection in Subkomponenten, wenn "untrusted user objects" oder deren Properties als React `type`, `props` und/oder `children` benutzt werden sollten (je nach Kombination, vergl. [Übersicht am Ende des Artikels (Link)](https://www.netsparker.com/blog/web-security/cross-site-scripting-react-web-applications/))
1619 |
1620 | ## 3.6.x SORMAS Rest API (bis zur aktuellen Version `v1.2.2`)
1621 |
1622 | _Abschnitt wurde nicht mehr abgeschlossen_
1623 |
1624 | Die (Ende Abschnitt 3.4) bereits benannnte Funktion `personsPush` ([Code Link](https://gitlab.com/lucaapp/web/-/blob/v1.2.1/services/health-department/src/network/sormas.js#L35)) ist wie folgt implementiert:
1625 |
1626 | ```
1627 | const personsPush = (traces, currentTime = Date.now()) =>
1628 | fetch(`${SORMAS_REST_API}/persons/push`, {
1629 | headers,
1630 | method: 'POST',
1631 | body: JSON.stringify(
1632 | traces.map(trace => ({
1633 | uuid: trace.uuid,
1634 | firstName: trace.userData.fn,
1635 | lastName: trace.userData.ln,
1636 | emailAddress: trace.userData.e,
1637 | phone: trace.userData.pn,
1638 | address: {
1639 | uuid: trace.uuid,
1640 | city: trace.userData.c,
1641 | changeDate: currentTime,
1642 | creationDate: currentTime,
1643 | street: trace.userData.st,
1644 | postalCode: trace.userData.pc,
1645 | houseNumber: trace.userData.hn,
1646 | addressType: 'HOME',
1647 | },
1648 | }))
1649 | ),
1650 | });
1651 |
1652 |
1653 | ```
1654 |
1655 | Für `userData` Objekte wird weder geprüft ob die erwarteten Properties vorhanden sind (ein Fehlen von keys wie `"fn"`, `"ln"` usw. würde vermutlich einen Crash im "Health Department Frontend" auslösen und die Auswertung von Gästelisten unmöglich machen), noch werden die Daten (Values) vorhandenen Properties geprüft. Durch Luca-Nutzer beliebig gestaltbare Daten, werden hier ungefiltert and die API eines nachgeschalteten Fachverfahrens weitergegeben (welches hoffentlich sauber mit dem Input umgeht).
1656 |
1657 | Um im Bild der Wasserhähne zu bleiben: Es gibt keine Mischbatterien, aber auch keine Durchlauferhitzer mehr. Sie lassen Ihrem Kind trotzdem ein Bad ein, setzen es ohne weitere Prüfung in die Badewanne und hoffen, dass es mit der Temperatur umgehen kann, die Sie gar nicht kennen.
1658 |
1659 | ## 3.7 Cross-Context Effekte aufgrund falscher Platzierung von Daten im "State" von ReactJS Komponenten
1660 |
1661 | _Abschnitt wurde nicht mehr begonnen_
1662 |
1663 | - Single-Source-Of-Trueth state in ReactJS Komponenten verursacht Crashes in verschiedenen Ausgabekontexten (je nach Input, z.B. im Render Kontext oder bei Vorbereiten von Exports)
1664 | - Wird für einen Kontext ein Filter angepasst, so dass ein Absturz verhindert wird, wird u.U. die Ausnutzung fehlender Filterung in einem anderen Output-Kontext möglich
1665 |
1666 | # Einstellung der Arbei an diesem Text
1667 |
1668 | Ich muss die Arbeit an diesem Dokumentation zum 19.06.2021 einstellen. Der Hersteller gibt mir keine Zeit die Zusammenhänge zu dokumentieren, welche zu Sicherheitslücken im Code führen und patcht stattdessen ständig Code mit neuen Fehlern nach.
1669 |
1670 | Um hier Schritt zu halten, müsste ich diese Dokument täglich überarbeiten, das ist nicht leistbar.
1671 |
1672 | Ich habe daher für die letzten beiden (dicht aufeinander folgenden Patches) nur noch Sicherheitslücken demonstrieren können, ohne diese zu dokumentieren.
1673 |
1674 | - v1.2.1 vom 17.06.21: [Demo Denial-Of-Service vom 18.06.21 (Video Link - Twitter)](https://twitter.com/mame82/status/1405855701830938627)
1675 | - v1.2.2 vom 18.06.21: [Demo CSV Injection vom 19.06.21 (Video Link - Vimeo)](https://vimeo.com/manage/videos/565012610)
1676 |
1677 | **Ich bitte LeserInnen die noch vorhandenen Schreibfehler im Dokument zu entschuldigen. Die Bearbeitung wurde ad-hoc abgebrochen (ohne weiteren Review).**
1678 |
1679 | Hinweise auf nicht dokumentierte Fehler:
1680 |
1681 | - weitere Fehler in der Ein- / AUsgabefilterung von Server-Version v1.1.13 bis v1.2.2
1682 | - weitere Fehler in anderen Ausgabekontexten (**insbesondere die SORMAS Rest API erhält vollkommen ungefilterte Daten aus nicht vertrauenswürdigen Quellen**)
1683 | - Fehler durch Durchmischung von Code zur Datenmanipulation und UI-Code
1684 | - bspw. triggert ein "mouse over" über einen Download Button das Data-Processing für **3 Ausgabekontexte**, ohne dass diese tatsächlich genutzt werden (falsches Verständnis von "reactive" UI)
1685 | - die selben Eingabedaten können (je nach Gestaltung) verschiedenste React Komponenten crashen (Web Rendering, Data Processing für Export etc..), weil Filterung nicht an den richtigen Stellen erfolgt oder ganz unterlassen wird
1686 | - weitere Externer Libraries, die sensitive Daten vom Luca-Code erhalten aber nicht dafür geeignet sind (vergleichbar zu `react-csv`) ... Es gibt kein sauberes Dependency Management
1687 | - diverse Angriffsmöglichkeiten im Frontend Code des Health-Department der aktuellsten Version (vergleiche hirzu die beiden letzten Video Links in diesem Dokument)
1688 | - Es wurden nicht für alle demonstrierten Sicherheitslücken CVEs requestet
1689 |
--------------------------------------------------------------------------------
/cve-luca/cve-2021-33838.md:
--------------------------------------------------------------------------------
1 | # CVE-2021-33838
2 |
3 | ## Description (CNA suggestion)
4 |
5 | Luca through 1.7.4 on Android allows remote attackers to obtain sensitive information about COVID-19 tracking because requests related to Check-In State occur shortly after requests for Phone Number Registration.
6 |
7 | ## Additional Information
8 |
9 | Exposure of Sensitive Information Through Data Queries vulnerability in
10 | the Android-Client of culture4life GmbH "Luca" - Covid-19 contact
11 | tracing System allows Luca backend operators (or attackers in control
12 | of the backend) to:
13 |
14 | 1. Associate an uninfected or traced Guest's Check-Ins to each
15 | other (conflicts with vendor-defined security objective 'O3')
16 | 2. Associate uninfected Guest's Check-Ins to the Guest phone number
17 | (conflicts with vendor-defined security objective 'O2')
18 | 3. Distinguish data sets of infected and uninfected Guests
19 |
20 | Cause of Issues:
21 |
22 | 1. This is caused by the behavior of the Android client, which
23 | continuously sends lists of overlapping 'TraceIDs' in intervals down to
24 | 3 seconds (depending on the app state) to poll for the App's
25 | check-in-state. Those TraceIDs are unique to the Android Client which
26 | generated them (they pseudonyms, calculated as hashes of globally
27 | unique user secrets). The fact that those TraceIDs are re-used in
28 | successive requests which are issued by Android Clients, allows to
29 | associate those requests to the same device, without utilizing further
30 | meta-data arriving at the backend. If one of those TraceIDs was used in
31 | a location checkin, the location data gets associated to the TraceID
32 | and stored by the backend without encryption. This allows to
33 | reconstruct full location histories on per device level, even if the
34 | device is rebooted or changes its public IP address.
35 | 2. This is caused
36 | by the fact, that the Android client sends additional meta-data (device
37 | manufacturer, device type, device OS version) with each
38 | backend-request, which, in combination with the device IP address,
39 | allows to associate "location check-in requests" to an initial
40 | registration request, in which the plain user's phone number is
41 | submitted for SMS-TAN Verification. In a normal usage scenario, the
42 | delay between the "phone number registration request" and the
43 | aforementioned "poll for check-in state requests" is less than one
44 | minute, which makes it easy to connect per-device location history to
45 | the device's phone number (PII).
46 | 3. If an authorized health department
47 | requests the location history of an infected guest from the backend,
48 | the backend learns about the hashes of the infection related TraceIDs.
49 | As the backend is able to reconstruct sets of TraceIDs belonging to a
50 | single user/Android Device, the same hashes could be calculated for
51 | these TraceID-sets. Ultimately those sets could be associated to
52 | infections traced by health departments.
53 |
54 | The issues have been summarized and published in a report and have been
55 | further explained in a video series (German). The vendor was made aware
56 | of this. The report was also included in a press statement of German
57 | "Chaos Computer Club".
58 |
59 | ## VulnerabilityType: Other
60 |
61 | CWE-202: Exposure of Sensitive Information Through Data Queries
62 |
63 | ## Vendor of Product
64 |
65 | culture4life GmbH
66 |
67 | ## Affected Product Code Base
68 |
69 | "Luca" - Covid-19 contact tracing system
70 |
71 | - version 1.7.4 and prior versions on Android
72 | - version 1.1.14 and prior versions on Web Backend
73 |
74 | ## Affected Component
75 |
76 | Backend, Android App
77 |
78 | ## Attack Type
79 |
80 | Local
81 |
82 | ## CVE Impact Other
83 |
84 | Backend operators or threat actors with backend access are able to learn location histories and PII of users of the system, which should only be possible for authorized health departments (as defined by vendor security objectives)
85 |
86 | ## Reference
87 |
88 | 1. https://github.com/mame82/misc/blob/master/luca_traceIds.md
89 | 2. https://luca-app.de/securityoverview/properties/objectives.html
90 | 3. https://www.youtube.com/playlist?list=PLKuX6iczGb3kuDsm2RFgbmRkTugkR9-UE
91 | 4. https://www.ccc.de/de/updates/2021/luca-app-ccc-fordert-bundesnotbremse
92 |
93 | ## Discoverer
94 |
95 | Marcus Mengs
96 |
--------------------------------------------------------------------------------
/cve-luca/cve-2021-33839.md:
--------------------------------------------------------------------------------
1 | # CVE-2021-33839
2 |
3 | ## Description (CNA suggestion)
4 |
5 | Luca through 1.7.4 on Android allows remote attackers to obtain sensitive information
6 | about COVID-19 tracking because the QR code of a Public Location can be intentionally
7 | confused with the QR code of a Private Meeting.
8 |
9 | ## Additional Information
10 |
11 | By manipulating QRCodes, locations owners are able to obtain
12 | unauthorized access to PII (first name, last name) of location guests,
13 | which should only be available to authorized health departments.
14 |
15 | The "luca" system distinguishes two types of locations:
16 |
17 | - **Type 1**: "Private Meetings" owned by a private person: Guests are able
18 | to check-in to a private meeting by scanning the QRCode, which is
19 | displayed by the app of the meeting owner. With the check-in the guest
20 | automatically provides her first and last name, encrypted with the
21 | public key of the "meeting owner". Ultimately, the meeting owner is
22 | able to decrypt and display those PII on the device running the Luca
23 | App. Neither the check-in data, nor the Guest's name are made available
24 | to external entities (including health departments). The Guest's app
25 | displays an information dialog, stating that "your host can see your
26 | first and last name".
27 |
28 | - **Type 2**: "Public locations" owned by registered "venue owners": Guests
29 | are able to check-in to a location by scanning the QRCode, which was
30 | generated by the Luca system and printed on paper by the registered
31 | venue Owner. In contrast to private meetings, no additional data should
32 | be provided to the location owner. Instead, an "encrypted contact data
33 | reference" gets transmitted to the "Luca" backend after the check-in,
34 | which gets encrypted with the public key of health departments
35 | (authorized for data access) and gets encrypted a second time with key
36 | material of the venue owner. A venue owner shall not able to learn any
37 | user data, which is assured because venue owners have no access to the
38 | private keys of health departments (inner encryption).
39 |
40 | A venue owner is able to make the QRCode of a "public location" (Type 1)
41 | appear to be a QRCode for a "private meeting" (Type 2). If a guest
42 | scans the QRCode with the Luca App the aforementioned information
43 | dialog for private meetings is shown, but chances are high, that the
44 | guest skips the dialog, as it makes no sense in the context of a
45 | physical location the guest wants to enter (to check-in to the location,
46 | a dialog confirmation is necessary). The "Luca" app applies no further
47 | checks, to determine if the target location is indeed a private meeting
48 | and ultimately sends the Guest's first and last name to the backend.
49 | The resulting data is only encrypted with the public key of the location
50 | (encryption for authorized health department is not applied anymore).
51 | Although the backend server is aware of the fact, that the check-in
52 | location is **not** a private meeting, the provided PII gets stored on the
53 | server and could from now on be accessed by the location owner (first and
54 | last name of guests, check-in and check-out timestamp).
55 |
56 | This behavior violates the following vendor defined security objectives
57 | (reference):
58 |
59 | - O1: An Uninfected Guest's Contact Data is known only to
60 | their Guest App
61 | - O2: An Uninfected Guest's Check-Ins cannot be
62 | associated to the Guest
63 |
64 | The issue was published in a demo video (Youtube, German, see
65 | reference). The vendor showed no intention to fix the issue, but made
66 | false claims on the root cause when the demo was also made available on
67 | Twitter (reference, Twitter statement of culture4live CEO).
68 |
69 | ## VulnerabilityType Other
70 |
71 | CWE-359: Exposure of Private Personal Information to an Unauthorized Actor
72 |
73 | ## Vendor of Product
74 |
75 | culture4life GmbH
76 |
77 | ## Affected Product Code Base
78 |
79 | "Luca" - Covid-19 contact tracing System
80 |
81 | - version 1.7.4 and prior versions on Android
82 | - version 1.1.14 and prior versions on Web Backend
83 |
84 | ## Affected Component
85 |
86 | Backend, Android Client
87 |
88 | ## Attack Type
89 |
90 | Context-dependent
91 |
92 | ## Impact Information Disclosure
93 |
94 | true
95 |
96 | ## Attack Vectors
97 |
98 | Location owner manipulates check-in QR-Code, which is scanned by Guests, in order to learn otherwise protected PII of the guest
99 |
100 | ## Reference
101 |
102 | 1. https://youtu.be/jWyDfEB0m08
103 | 2. https://luca-app.de/securityoverview/properties/objectives.html
104 | 3. https://twitter.com/patrick_hennig/status/1387738281757061125
105 |
106 | ## Discoverer
107 |
108 | Marcus Mengs
109 |
--------------------------------------------------------------------------------
/cve-luca/cve-2021-33840.md:
--------------------------------------------------------------------------------
1 | # CVE-2021-33840
2 |
3 | ## Description (CNA suggestion)
4 |
5 | The server in Luca through 1.1.14 allows remote attackers to cause a
6 | denial of service (insertion of many fake records related to COVID-19)
7 | because Phone Number data lacks a digital signature.
8 |
9 | ## Additional Information
10 |
11 | The "Luca" System assures verified phone numbers for user participating
12 | in the system, in order to provide authorized health departments a
13 | possibility to contact users which were involved in a Covid-19
14 | infection-chain. No other user provided contact data gets verified, so
15 | it is crucial for the integrity of the system, that the verification
16 | process for the phone number is working.
17 |
18 | There are two relevant steps in the registration workflow:
19 |
20 | - **Step 1**: The Luca-Client (f.e. Android App) transmits the user's phone
21 | number to the Luca-backend, which then issues a 3rd party service to
22 | send a verification SMS to the provided phone number. Once the user
23 | enters the TAN a second request is made to the backend. The response to
24 | the second request indicates if the submitted TAN was correct.
25 | - **Step 2**: Once step 1 was completed, the client continues to send the
26 | encrypted form of the user's contact data to the backend and receives
27 | an assigned "user ID" in response, indicating that the user was
28 | registered.
29 |
30 | With regards to the two steps described above, the backend server
31 | behaves stateless, which means the Luca-client decides if it moves on
32 | to step 2 or not, based on the result of step 1. As the client is
33 | controlled by the user, the first step could be skipped easily,
34 | ultimately bypassing the whole verification process.
35 |
36 | This allows to create an unlimited amount of invalid user accounts and
37 | trigger check-ins of those accounts to various locations. Ultimately
38 | authorized health-departments are posed to the risk of getting flooded
39 | with invalid data, during attempts to trace infections (every infection
40 | contact has to be verified manually).
41 |
42 | The issue is publicly known since the release of the system, but was
43 | not addressed by the vendor, so far.
44 |
45 | A possible mitigation was discussed in the gitlab repo of the vendor,
46 | but not deployed (see gitlab reference)
47 |
48 | ## VulnerabilityType Other
49 |
50 | CWE-345: Insufficient Verification of Data Authenticity
51 |
52 | ## Vendor of Product
53 |
54 | culture4life GmbH
55 |
56 | ## Affected Product Code Base
57 |
58 | "Luca" - Covid-19 contact tracing system
59 |
60 | - version 1.1.14 and prior versions on Web Backend
61 |
62 | ## Affected Component
63 |
64 | Backend, Health department frontend
65 |
66 | ## Attack Type
67 |
68 | Remote
69 |
70 | ## CVE Impact Other
71 |
72 | Improper verification of phone numbers allows to create invalid accounts, which could be checked-in to Luca-location in order to increase workload on authorized health departments when tracing COVID-19 contacts
73 |
74 | ## Attack Vectors
75 |
76 | Bypass of SMS-TAN based phone number verification during user registration process
77 |
78 | ## Reference
79 |
80 | 1. https://luca-app.de/securityoverview/processes/guest_registration.html#verifying-the-contact-data
81 | 2. https://gitlab.com/lucaapp/web/-/issues/1#note_560963608
82 |
--------------------------------------------------------------------------------
/luca_tracking.md:
--------------------------------------------------------------------------------
1 | # Tracking of unique mobile devices and check-in locations from operator perspective of LucaApp backend
2 |
3 | Author: Marcus Mengs (MaMe82)
4 |
5 | ## TL;DR
6 |
7 | Based on the observation of network traffic between the Luca-app and the Luca backend, it could be concluded that an observer (f.e. backend operator) is able to:
8 |
9 | - continuously and uniquely re-identify mobile devices:
10 | - across application restart
11 | - across device reboot
12 | - across IP-address changes
13 | - ... and associate plain data of visited locations (check-in / check-out) to those devices
14 |
15 | **...without any involvement of responsible health departments or location owners**
16 |
17 | ## Disclaimer
18 |
19 | _The content of this document is based on my personal observation of the HTTP communication of a single test device running the Luca app, thus I do not consider it being representative (without further verification). It is neither a full fledged analysis of the Luca ecosystem, nor a representative study. I take no responsibility for the abusive use of information given in this documents. I DO NOT GRANT PERMISSIONS TO USE CONTAINED INFORMATION TO BREAK THE LAW. The document is provided "as is"._
20 |
21 | ## Introduction
22 |
23 | By design of the **LucaApp** architecture deploys various asymmetric and symmetric keys for different purposes across involved entities. The goal: protect user data from disclosure.
24 |
25 | The detailed security objectives are described here: [link to security concept](https://luca-app.de/securityconcept/properties/objectives.html#objectives)
26 |
27 | One out of multiple symmetric keys (or "secrets") is the `tracing secret`, which is used to generate `tracingIDs` for anonymized user check-ins into dedicated locations (in luca's terminology users are called `guests` and locations, which offer check-ins, are called `venues`).
28 |
29 | The "security concept" describes a [tracingID](https://luca-app.de/securityconcept/properties/secrets.html#term-trace-ID) like this:
30 |
31 | ```
32 | An opaque identifier derived from a Guest’s user ID and tracing secret during Guest Check-In. It is used to identify Check-Ins by an Infected Guest after that Guest shared their tracing secret with the Health Department.
33 | ```
34 |
35 | The term `opaque` implies that **at no point in time, luca operators are able to draw conclusion on the guest, which produced a `trace ID`.**
36 |
37 | Moreover, the `trace IDs` are meant to allow legit health departments (**and only health departments**) to reconstruct a guest' check-in history. A detailed description could be found in the process [Tracing the Check-In History of an Infected Guest](https://luca-app.de/securityconcept/processes/tracing_access_to_history.html#process-tracing). Below a short excerpt:
38 |
39 | ```
40 | The first part of the contact tracing is for the Health Department to reconstruct the Check-In History of the Infected Guest. Each Check-In stored in luca is associated with an unique trace ID. These IDs are derived from the tracing secret stored in the Guest App (as well as from the Guest’s user ID and a timestamp). Hence, given the Infected Guest’s tracing secrets the Health Department can reconstruct the Infected Guest’s trace IDs and find all relevant Check-Ins.
41 | ```
42 |
43 | In the 'security considerations' of said process description, the security concept also mentions the possible [Correlation of Guest Data Transfer Objects and Encrypted Guest Data](https://luca-app.de/securityconcept/processes/tracing_access_to_history.html#security-considerations)
44 |
45 | ```
46 | After receiving a Infected Guest’s guest data transfer object the Health Department Frontend uses the contained user ID to obtain that Guest’s encrypted guest data from the Luca Server. This is done in order to display the Infected Guest’s Contact Data to the Health Department.
47 |
48 | The Luca Server can (indirectly) use this circumstance in order to associate a guest data transfer object with the encrypted guest data of the same Guest by observing the Health Department Frontend’s requests
49 | ```
50 |
51 | Based on my own observations of the behavior of the LucaApp (Android, version 1.4.12), the Luca-backend is able to uniquely identify devices (even across connectivity loss and IP-Address changes) and able to associate location check-ins to those devices **without any involvement of health departments**. I am going to describe aforementioned observations and my personal conclusion throughout this document.
52 |
53 | ## Side note on: device tracking versus user tracking
54 |
55 | For most real world cases, it is sufficient for 3rd party trackers to identify devices with a high probability of uniqueness. This is because, it a single mobile device is used by a single user. There also exist additional tracking technologies with the goal of tracking dedicated users across multiple device (cross-device tracking) which are not in scope of this review.
56 |
57 | It is also known, that, while luca takes efforts to protect the actual user data (name, address, phone number etc), the authenticity of said user data can not be assured by the luca-service. This is even true for the phone number! It was shown multiple times, that the deployed 'SMS TAN verification' could be bypassed, because it is implemented on client side (user controlled).
58 |
59 | This leads to the conclusion, that the encrypted user data (which the luca backend holds ready for health departments) isn't necessarily of value. But of course, meta-information which arises at the luca backend and allows device-tracking and behavior-tracking as described above **is of exceptional value for every tracking service**.
60 |
61 | # Review of relevant network interaction between luca Android app and luca-backend
62 |
63 | In this section I am going to review HTTP communication between the luca app and the backend, with focus on the `/traces/bulk` endpoint. Communication to other endpoints (e.g. user registration) is omitted, where it does not add up to the topic of this document.
64 |
65 | The HTTP body data excerpts used to illustrate observations, reflect real data of HTTP communication of app and backend. In order to review this communication, a luca test account was created (SMS verification was skipped, in order to allow health departments to easily recognize the phone number in use as being invalid).
66 |
67 | Additional notes:
68 |
69 | - In order to observe check-in/check-out behavior, one of multiple publicly-shared location QR-codes has been used for self check-in. As those QR codes are already publicly available, no efforts have been taken to obfuscate related location data, which occurs in the HTTP responses by the luca-backend endpoints.
70 | - The production API `https://app.luca-app.de/api/v3/` was used for testing. A staging API is available at `https://staging.luca-app.de/api/v3/`, but using it would involve changes in the application code. As the provided Android source code is incomplete, it is not possible to compile an adjusted version of the app. Runtime-modification of the app by other means (to redirect API requests to the staging API) have not been applied for obvious reasons.
71 |
72 | ## 1. Classifiers in HTTP request headers
73 |
74 | As I mostly present HTTP body data in this document, I want to make pretty clear that **each HTTP request from the luca app provides additional device classifiers to the backend via request headers**. Those classifiers are:
75 |
76 | 1. The Android OS version
77 | 2. The Device Manufacturer
78 | 3. The Device Model
79 |
80 | The Luca app always assures that those classifiers are included, by enforcing a `User-Agent` string which is constructed like this ([link to code](https://gitlab.com/lucaapp/android/-/blob/master/Luca/app/src/main/java/de/culture4life/luca/network/NetworkManager.java#L127)):
81 |
82 | ```
83 | private static String createUserAgent() {
84 | String appVersionName = BuildConfig.VERSION_NAME;
85 | String deviceName = Build.MANUFACTURER + " " + Build.MODEL;
86 | String androidVersionName = Build.VERSION.RELEASE;
87 | return "luca/" + appVersionName + " (Android " + androidVersionName + ";" + deviceName + ")";
88 | }
89 | ```
90 |
91 | For a request from my test device, the resulting HTTP Header looks like this:
92 |
93 | ```
94 | User-Agent: luca/1.4.12 (Android 9;samsung SM-G900F)
95 | Content-Type: application/json
96 | Content-Length: 15
97 | Host: app.luca-app.de
98 | Connection: Keep-Alive
99 | Accept-Encoding: gzip
100 | ```
101 |
102 | It could not be avoided that the luca-backend also receives the public IP-Address of the user **for each HTTP request**. For most mobile data connections, public IP-Addresses are shared by multiple users. Additional identifiers, as used in this case, greatly increase the probability to uniquely distinguish requesting devices, even if they share the same IP-Address.
103 |
104 | This problem of the luca architecture was covered in multiple reviews. Thus I want to focus on how 'trace IDs' could be used in order to increase the probability of identifying devices uniquely.
105 |
106 | For the rest of the review, I will only cover HTTP body data, but it is crucial to keep in mind, that each and every request involves aforementioned classifiers, the IP-address (as identifier) and a timestamp.
107 |
108 | ## 2. Communication after application startup
109 |
110 | When the application is started the first time, a user account has to be created. Once that is done, the app creates the various crypto keys - including the 'tracing secret' - which is only known locally.
111 |
112 | After 'tracing secret' creation, the app ultimately starts to derive `trace IDs`. Those `trace IDs` are re-generated every 60 seconds, as described in the documentation. The documentation is less specific when it comes to backend-polling of `trace IDs`. The topic is touched in the process [Check-In via Mobile Phone App](https://luca-app.de/securityconcept/processes/guest_app_checkin.html#process-guest-checkin) of the documentation, which states:
113 |
114 | ```
115 | This polling request might leak information about the association of a just checked-in trace ID and the identity of the Guest (directly contradicting O2). As mobile phone network typically use NAT, the fact that the Luca Server does not log any IP addresses and the connection being unauthenticated, we do accept this risk.
116 | ```
117 |
118 | So, what I described under `1. Classifiers in HTTP request headers` is an handled with "we do accept this risk". Again, I want to emphasize, that this statement refers to the user's IP-Address (not avoidable), not to the additional classifiers introduced by the app itself (not necessary).
119 |
120 | So let's have a look, how frequently the polling occurs, to get a better picture. The polling is handled by the already mentioned Endpoint `/traces/bulk`:
121 |
122 | ```
123 | ..snip..
124 | 07:29:48 HTTPS POST app.luca-app.de /api/v3/traces/bulk
125 | 07:29:51 HTTPS POST app.luca-app.de /api/v3/traces/bulk
126 | 07:29:54 HTTPS POST app.luca-app.de /api/v3/traces/bulk
127 | 07:29:57 HTTPS POST app.luca-app.de /api/v3/traces/bulk
128 | 07:30:00 HTTPS POST app.luca-app.de /api/v3/traces/bulk
129 | 07:30:03 HTTPS POST app.luca-app.de /api/v3/traces/bulk
130 | 07:30:06 HTTPS POST app.luca-app.de /api/v3/traces/bulk
131 | 07:30:09 HTTPS POST app.luca-app.de /api/v3/traces/bulk
132 | ..snip..
133 | ```
134 |
135 | So when the app is running in foreground, the **endpoint is polled in a 3 second interval**.
136 |
137 | In contrast to the (not very specific) process description in [Check-In via Mobile Phone App](https://luca-app.de/securityconcept/processes/guest_app_checkin.html#process-guest-checkin), **the polling happens all the time, not only after a check-in**. Therefor the app is polling from the very beginning, even if it wasn't used to create a single check-in.
138 |
139 | What about the content of the polling requests?
140 |
141 | ### Body data of POST request to `https://app.luca-app.de/api/v3/traces/bulk`:
142 |
143 | ```
144 | {
145 | "traceIds": [
146 | "H2Tceqpx1yAl5ej3Mk1aAg==",
147 | "BHEHUaHvu0du0B0lobzVGw==",
148 | "50zVzW7ZS5+qPjKfPhdjsg==",
149 | "E4Rr5NtfbVvPXkoPxnUqew==",
150 | "s+yvuYyw6t78lGazu5VK1Q=="
151 | ]
152 | }
153 | ```
154 |
155 | For each polling request (every 3 seconds) a set of device-generated 'trace IDs' is sent to the endpoint. This 'trace ID set' could safely be regarded as a user pseudonym. This is because each contained 'trace ID' was derived from the non-public 'tracing secret' which is unique the user (typically no other `tracing secret` would generate the same `trace IDs`). The chance that another user generates a 'trace ID' which is equal to an ID in the set from above is close to zero. This is because the ID is generated as `trace_id = HMAC-SHA256(user_id || timestamp, tracing_secret) # truncated to 16 bytes` **with a very low probability of collisions**. Even for the unlikely case, that a redundant 'trace ID' would be generated by another user, the combination of multiple 'trace IDs' in a set would allow to distinguish them clearly.
156 |
157 | **To sum up: If the same 'trace IDs' are used across multiple requests, they could correlated to the same unique user device along with the meta-data contained in the request, even if the device IP address has changed throughout successive requests.**
158 |
159 | _Note: I was using the terms `user` and `device` interchangeably, because the user's `tracing secret` from which the IDs are generated, is unique per device. While calculated `trace IDs` are related to the luca-user, they do not reveal any contact data. Yet, it should be clear that `trace IDs` meet all requirements to serve as unique device identifier. The circumstance that many `trace IDs` could be generated per device, does not change this fact!_
160 |
161 | The `trace ID set` which gets sent every 3 seconds, continuously grows, as a newly generated `trace ID` is added every 60 seconds (the interval in which trace IDs are re-generated). For the tracing secret, the documentation states the following:
162 |
163 | ```
164 | Moreover, the tracing secret is rotated on a regular basis in order to limit the number of trace IDs that can be reconstruced when the secret is shared.
165 | ```
166 |
167 | The `tracing secret rotation` **does not change the fact, that a `trace ID` could be used as device identifier** (which is also is not the purpose of the rotation).
168 |
169 | ## 3. What the luca-backend has learned so far
170 |
171 | According to the observation, the luca-backend learned how a unique set of `trace IDs` is associated to a single device, where the device is represented by the following classifier set:
172 |
173 | - requesting IP-Address (could change)
174 | - OS version (unlikely to change often)
175 | - device manufacturer (persistent)
176 | - device model (persistent)
177 |
178 | Moreover, if the device' IP-address changes (f.e. after disruption of the mobile data connection), the `trace IDs` could be used to re-identify the same device across IP-Address changes, if a `trace ID` re-appears in a request after the address change.
179 |
180 | So are `trace IDs` re-used across multiple backend requests? Yes, they are. As pointed out, the set of `trace IDs` sent to the endpoint `/traces/bulk` grows continuously, while new IDs are generated (up till a condition, which I will cover later), and gets transmitted to the backend every 3 seconds, when the app is in foreground. **Moreover, the device-unique `trace ID set` which gets transmitted, survives the following conditions without changes:**
181 |
182 | - temporary connectivity loss
183 | - temporary connectivity loss with IP address change
184 | - application restart
185 | - **device reboot**
186 |
187 | Even if the device gets rebooted, as soon as the app starts again, the same identifier set gets sent (with new `trace IDs` appended, as they are generated).
188 |
189 | The following additional facts are worth noting, for `trace ID sets` transmitted to `/traces/bulk`:
190 |
191 | - no invalid trace IDs are introduced (no artificial error or bias is introduced)
192 | - chronological order of trace IDs (last ID is always the one used, when the QR code gets scanned for self check-in)
193 |
194 | So at which point in time does this "trace ID set" change? It changes once a check-in occurs!
195 |
196 | ## 4. Self check-in
197 |
198 | To further analyse the polling behavior, it was necessary to do a `self check-in` against a publicly known location (Internet-published check-in QR code). The app sends the following POST request body data to the endpoint `/traces/checkin` to do so:
199 |
200 | ```
201 | {
202 | "data":
203 | "9XRuN771VktvWNfGbaxfg86qRxlqYe6I/CyBNCSG4a9htSkXMv4rVSGTVxzKlQjGzVMMpJQr1uDpmVAIkV9ciYcULydZS8n5hDRL",
204 | "deviceType": 1,
205 | "iv": "iHEzRMhhSrNSRR61OcMhRA==",
206 | "mac": "K8exxD+s1sHT026Muvwlz2yQ4Ij/NdTmLkfe3yNtgTc=",
207 | "publicKey": "BHuLR1yt98FsTfcnqv6IkSKw8Hn9EA597/ojKqxEz+zgL8RXhn/qRafQakqYSPE2CnxiY6oBYIF17ZqH5aZCksA=",
208 | "scannerId": "ec11e236-cf8a-419a-8644-68c56d8b8939",
209 | "timestamp": 1618295400,
210 | "traceId": "CbP29lubGaDIUD+QWJkcPg=="
211 | }
212 | ```
213 |
214 | In case of a "scanner check-in" this data would be sent to the luca-backend by the "scanner frontend", in case of "self check-in" this request gets sent by the user device. Distinguishing the two cases does not matter for my considerations, as the data always involves the specific `trace ID` used for the check-in (which is already tied to the user device which generated it). Remember: The backend already learned about how `trace IDs` are associated to a user device, even if the IP-Address changes or the device is rebooted.
215 |
216 | The next step is the one, which is described for the [guest check-in process](https://luca-app.de/securityconcept/processes/guest_app_checkin.html#process) (polling should only happen after check-in):
217 |
218 | ```
219 | Therefore, the Guest App polls the Luca Server via an unauthenticated connection. This inquires whether a Check-In was uploaded by a Scanner Frontend with a trace ID that the Guest App recently generated. Once this inquiry polling request is acknowledged by the Luca Server, the Guest App assumes that a successful QR code scan and Check-In was performed. Some UI feedback is provided to the Guest.
220 | ```
221 |
222 | The user device continues to poll the `/traces/bulk` endpoint with the list of generated `trace IDs` including the one which was used for check-in:
223 |
224 | ### Body data of POST request to `https://app.luca-app.de/api/v3/traces/bulk` after checkin:
225 |
226 | ```
227 | "traceIds": [
228 | "H2Tceqpx1yAl5ej3Mk1aAg==",
229 | "BHEHUaHvu0du0B0lobzVGw==",
230 | "50zVzW7ZS5+qPjKfPhdjsg==",
231 | "E4Rr5NtfbVvPXkoPxnUqew==",
232 | "s+yvuYyw6t78lGazu5VK1Q==",
233 | "x1AgyFZg9Y6QcCUsAGEzDw==",
234 | "72EKGBGsDGpL6JB1EI1p4w==",
235 | "kNTZZ7Zs0Bb9shXSvlGeJw==",
236 | "/DI7FvPnlf8bQR3VKWy8cA==",
237 | "iHxdgTRB7q+sC19Z806urQ==",
238 | "iHxdgTRB7q+sC19Z806urQ==",
239 | "AwjP8y56D1ZdhtDM642EKA==",
240 | "CbP29lubGaDIUD+QWJkcPg=="
241 | ]
242 | ```
243 |
244 | While previous requests received an empty JSON array `[]` in response the post-checkin-request receives the following response:
245 |
246 | ```
247 | [
248 | {
249 | "checkin": 1618295400,
250 | "checkout": null,
251 | "createdAt": 1618295427,
252 | "locationId": "866170ab-0d0a-44ca-b441-1fd6e02b3579",
253 | "traceId": "CbP29lubGaDIUD+QWJkcPg=="
254 | }
255 | ]
256 | ```
257 |
258 | So the last `trace ID` for which the device was polling, is now associated to a `location ID`. The fact, that the last `trace ID` in the set used for polling requests, was also the one used for the actual check-in, does not even matter. This is, because the response itself includes the exact `trace ID` used to check-in, but now, the `trace ID` associated to the `locationID`. It is out of question, if the luca-backend has learned about which device is checked-in to which location, as it provides the plain information itself.
259 |
260 | Before moving on, I want to define the phrase `"...the luca-backend learned..."` more precisely: All observations are based on monitoring legacy HTTP traffic between the app and the backend. This involves interception of the underlying TLS connection. As the luca-backend has to terminate the TLS connection at some point, I am not only talking about identifiers and classifiers learned by the luca-backend operators. The same information is available to all intermediaries placed behind the front-facing TLS endpoint (e.g. proxies, load balancers, WAF providers etc). One could conclude, that intermediaries do not learn about the user's source IP, but this does not hold true, because most intermediaries include the requesters IP address in additional HTTP headers, to preserve it to for actual application server (f.e. `X-Forwarded-For` header). From now on, I will use the term **observer** for to describe an entity which is able to look into plain HTTP content, this **always involves luca backend operators**!
261 |
262 | Moving on...
263 |
264 | Once the app received the `loactionId` for the `trace ID` which was used for check-in against the backend, the app **immediately** requests additional (plain) location data from the endpoint `https://app.luca-app.de/api/v3/locations/{locationId}`
265 |
266 | ### Response body for GET request towards `https://app.luca-app.de/api/v3/locations/866170ab-0d0a-44ca-b441-1fd6e02b3579`
267 |
268 | ```
269 | {
270 | "city": "Büchen",
271 | "createdAt": 1617001391,
272 | "firstName": "",
273 | "groupName": "Bürgerhaus",
274 | "lastName": "",
275 | "lat": 53.48026,
276 | "lng": 10.61603,
277 | "locationId": "866170ab-0d0a-44ca-b441-1fd6e02b3579",
278 | "locationName": "Sitzungssaal",
279 | "name": "Bürgerhaus - Sitzungssaal",
280 | "phone": "",
281 | "publicKey": "BIb7wN2dShGNOXbzQq8wfW7Q/iv3jWrQSSFbkqjO6O9HuKR1WSxRpAfxYdKByN31qe8HHn+Evnq289RDXHoNtaU=",
282 | "radius": 0,
283 | "state": "Schleswig-Holstein",
284 | "streetName": "Amtsplatz",
285 | "streetNr": "1",
286 | "zipCode": "21514"
287 | }
288 | ```
289 |
290 | ## 5. At this point, an observer of the plain HTTP content has the following information:
291 |
292 | - checkin `trace ID` (associated to a device, even after reboot, connectivity loss, IP-address change or app restart)
293 | - checkin location, with all relevant data
294 | - checkin time
295 |
296 | From the single request to `https://app.luca-app.de/api/v3/locations/{locationId}` alone, an observer learns:
297 |
298 | - plain location data
299 | - high probability for a check-in of the requesting device (IP address, device brand, device model, OS version), because the request appears immediately after check-in
300 |
301 | Even if an observer is only able to monitor the request method and URL (f.e. a WAF protecting the endpoint, load balancers, log servers etc), she could draw the conclusion that the request is associated to a check-in (of the requesting device) which happened at this exact point in time. The location could be directly derived from the URI path, which includes the `locationID`.
302 |
303 | In fact, the only thing which is not known to an observer or the backend operators is the content of the `encrypted contact data` (which, again, isn't of much value, because it does not have to be valid).
304 |
305 | ## 6. post-check-in behavior
306 |
307 | Once the user has checked in to a location, the data set used to poll `/traces/bulk` changes for the first time.
308 |
309 | ### Body data of POST request to `https://app.luca-app.de/api/v3/traces/bulk` after check-in:
310 |
311 | ```
312 | traces3_req={
313 | "traceIds": [
314 | "CbP29lubGaDIUD+QWJkcPg=="
315 | ]
316 | }
317 | ```
318 |
319 | The data set now only includes the `trace ID` used for the most recent check-in. No new IDs are added to the set anymore (the app generates no new QR codes, as the UI shows the checkout dialog, now).
320 |
321 | Also, to be more precise, this behavior change dos not occur directly after check-in (there have already been requests with a larger `trace ID sets`, which included the check-in `trace ID` and received the associated `locationID` in response). Instead, the behavior changes after the successful request to the aforementioned endpoint `/locations/{locationId}`. This, again, allows an observer to confirm a successful check-in if she only monitors `/locations/{locationId}`.
322 |
323 | In addition, the continuous polling of a **single** `trace ID` allows to draw the conclusion that the device is checked-in into a location, using this exact `trace ID` (The `trace ID set` would otherwise get a new `trace ID` appended after 60 seconds. The minimum time interval before a possible checkout is enforced to 60 seconds, too). The check-in location itself, is provided in each HTTP response, now:
324 |
325 | ```
326 | [
327 | {
328 | "checkin": 1618295400,
329 | "checkout": null,
330 | "createdAt": 1618295427,
331 | "locationId": "866170ab-0d0a-44ca-b441-1fd6e02b3579",
332 | "traceId": "CbP29lubGaDIUD+QWJkcPg=="
333 | }
334 | ]
335 | ```
336 |
337 | So there is not even a need to monitor `/traces/bulk` continuously. A single request, which holds only one `traceId` and receives a `locationId` in response, could be safely assumed to indicate, that the device is currently checked-in to this exact location.
338 |
339 | Such a request is sent every 60 seconds, now (increased polling interval, while user is checked in to a location).
340 |
341 | ## 7. Checkout
342 |
343 | The checkout does itself does not add much information, with respect to the scope of this document. But it is worth mentioning, because of some other aspects.
344 |
345 | ### checkout POST request body against https://app.luca-app.de/api/v3/traces/checkout
346 |
347 | ```
348 | {
349 | "timestamp": 1618296420,
350 | "traceId": "CbP29lubGaDIUD+QWJkcPg=="
351 | }
352 | ```
353 |
354 | For the checkout, the app provides a timestamp along with the `trace ID` associated to the checkin location. While the backend API places some measures against invalid timestamps (for example sending a checkout timestamp which is smaller than the checkin timestamp produces a 409 response), but an attacker could send random 'trace IDs' with a recent timestamp, to check-out random luca-users. This comes down to brute-forcing of valid 'trace IDs' and shall be countered by rate limiting. As the scenario is not in scope of this document, no tests for proper rate limiting have been carried out.
355 |
356 | ## 8. post checkout behavior
357 |
358 | Once the user has checked out, the poling behavior against `/traces/bulk` is the same as described in section `2. Communication after application startup`. Before polling starts, again, the list of polled `trace IDs` is flushed. Still all conditions are met to allow an observer to track a unique device across polling requests (even if the IP-Address changes).
359 |
360 | There is a single request, which could not be associated to a unique device, based on the transmitted `trace IDs`, as it just contains no `trace ID`. This is the very first request to `/traces/bulk` after the checkout. This is likely, because the next `trace ID` generated by the app was not put to the flushed `trace ID set` before the first polling request was sent. Anyways, this is only true for 3 seconds (which is the new polling interval), as the 2nd request contains a `trace ID`, again.
361 |
362 | ## Summary of information available to an observer of the `/traces/bulk` endpoint
363 |
364 | 1. This endpoint continuously receives HTTP requests, which include `trace IDs` which are unique to a single mobile device participating in the luca ecosystem.
365 |
366 | 2. While the `trace IDs` are suitable to uniquely identify a device, each request includes additional device classifiers (not covered by data protection laws). Those identifiers are not only usable for device fingerprinting. Given the fact that the Luca-system was designed to be extended with interfaces for services which offer less anonymity (f.e. event ticket handling), it should be kept in mind, that the device classifiers collected with each request (IP address, OS Version, device manufacturer, device model, request timestamp) could easily be associated to the same classifiers collected by "other services" for a large time window. This especially gets a problem, if those "other services" are operated by entities which involved in luca-backend operation (which includes possibly includes providers of intermediary sub-services like WAF, DDoS protection etc.).
367 |
368 | 3. As the `trace IDs` have the property of being unique to a device, they could be used to associated different requests against the endpoint to the same device, in case they are reused throughout successive requests. In fact, not only a single `trace ID` is reused, instead whole sets of `trace IDs` are sent to the endpoint by each device, with a high amount of overlap per participating device. This not only is an enabler for continuos device tracking, it also allows to associate different requests to the same device while its IP-Address has changed, ultimately allowing full-fledged behavior analysis.
369 |
370 | 4. A device's check-in state is known, by observing the endpoint for more than 60 seconds:
371 |
372 | 4.1 If a device is not checked-in to a location, the device polls the endpoint in a **3 second interval**, with a continuously growing set of `trace IDs`. Multiple, successive requests of the same device could be associated to each other, based on overlapping `trace IDs`, even if the IP-Address and additional classifiers are disregarded. The state of a `trace ID set` used by a participating device to poll against `/traces/bulk` even survives device a reboot.
373 |
374 | 4.2 If a device is checked-in to a location, the device polls the endpoint in a **60 second interval**, with a **single** `trace ID`. This trace ID is the one, which was used to check-in to the location. The `locationID` which could be used to obtain detailed plaintext information on the location, is contained in the HTTP response (additional location information could be retrieved from other endpoints, as detailed in this document). Multiple, successive requests of the same device could be associated to each other,based on the single `trace ID`, even if the IP-Address and additional classifiers are disregarded. _Note: If a user is not checked in to a location, a request with a single `trace ID` could still occur, but the response would not contain a `locationId` - also the polling interval would be 3 seconds, not 60 seconds_
375 |
376 | It should also be noted, that the "security concept" does not clearly state that `trace IDs` are transmitted, whenever the luca app is running in foreground. If anything, it explains that the polling starts after check-in (QR code scan) and ends after UI confirmation of the check-in.
377 |
378 | ## Conclusion
379 |
380 | The only information which can not be obtained by observing the `/trace/bulk` endpoint, is the actual user contact data. This isn't worth much, as it has been proven multiple times, that random user contact data could be provided to the luca ecosystem (because the validation could be bypassed, which also affects the provide mobile number). The ability to analyse mobile device behavior as described above, **does not require any interaction with health departments or location owners**. Not only backend operators are able to obtains those information, also every intermediary service behind the front-facing TLS endpoint is able to do so. This of course includes possible attackers, which remain undetected.
381 |
382 | ### Personal note:
383 |
384 | The fact that the luca-backend is able to track a unique device accross IP-address changes (based on the unique set of polled tracingIDs) is not only questionable in terms of privacy, it also appears to be absolutely unnecessary. The same is true for the collection of additional device classifiers in the User-Agent string, they just have no proper use in the advertised anonymous check-in tracing system.
385 |
--------------------------------------------------------------------------------