Thema: Server vor invalidem Input schützen oder Wie mache ich den Server kaputt? [interaktive Session] [Folien online verfügbar] [PDF herunterladen] [prüfungsrelevante Themen in den Folien im Gitrepo, großer Überlapp]
Zuerst ein Rückblick: Was haben wir im Semester bisher gemacht? 1. Mit Express HTTP-Server intialisieren und REST-Endpoints definieren 2. HTML-Rendering mit EJS, um HTML-Snippets zu generieren und auf einer Webseite einbinden 3. Anbindung an MongoDB mit dem mongodb NPM package 4. Datenbankschemata mit Mongoose erstellt 5. letzte Woche: 2-way Kommunikation mit Websockets, damit alle Kommunikationspartner Nachrichten initiieren können 2 Themen kommen heute dazu
Erste Architektur mit Express und MongoDB 1. Client, Web application, Browserfenster 2. Express-Server unabhängig von HTTP-Server, Websockets oder andere Kommunikation 3. MongoDB könnte jede andere Datenbank sein
Nachteile von dieser Architektur: 1. Müll als Inputdaten führt zum Verlust von Vertrauen in Daten, die vom Server kommen 2. Müll als Query-Daten führt zum Verlust in Daten in der Datenbank
Mehr noch: Verlust von Kontrolle über die geschriebenen Daten -> Datenbank wird komplett marode -> Datenbank stürzt ab oder wird unbrauchbar
Infolgedessen kommt der Server auch in Stress, mit den Anfragen klar zu kommen. Nach kurzer Zeit ist auch der Server nicht mehr erreichbar, weil er überfordert ist.
Vor Weihnachten haben wir mit Mongoose definiert, wie Daten aussehen müssen, die in die Datenbank geschrieben werden. Die Datenbank wird dadurch zumindest funktional abgesichert. Dadurch wird das Vertrauen in die Daten, die von der Datenbank kommen, wieder hergestellt. Der Server bleibt aber unsicher, weil auch Daten ankommen können, die nicht vorhergesehen werden.
Frage: Was für schlechte Inputs können wir erfahren? Beispiel: Twitter/X { "authorId" : "ElonMusksUserId", "content" : "Mark Zuckerberg is a great business man.", "creationTime": 1705326917363 } [Brainstorming] - Unflätiger oder illegaler Inhalt - Unlogischer Inhalt - Fehlendes Feld / Leeres Feld - Falsches Format
- Unflätiger oder illegaler Inhalt - Unlogischer Inhalt - Fehlendes Feld / Leeres Feld - Falsches Format
Häufige und relativ simple Atacke: SQL Injection [Wer kennt SQL Injection?]
Wiederholung, was alles falsch laufen kann
Lösung: Inputvalidierung mit JOI 1. NPM package 'joi' 2. Schema definieren mit Funktionen: Object 1. String -> alphanumerisch -> required 2. String -> Mindestlänge 1 -> required 2. Number -> Integer -> Mindestwert: Jetzt - 1 Minute -> Maximalwert: Jetzt -> required 3. data 4. validierung mit `schema.validate(data)`
Viel mehr Funktionen für Validierung - Password Regex Pattern: alphanumerisch, 3-30 Zeichen - repeat_password: referenzieren eines Schemas mit Joi.ref() - optionale Optionen, ob String oder Number, nicht required - Für Datentypen gibt es Formate, z.B. email oder Integer [Link für mehr Möglichkeiten]
Eingebunden in Express-Server 1. Schema definieren 2. Mit Request Body validieren 3. Bei Error 422, andernfalls createPost() - Schema nicht wiederverwendbar - Business-Logik vermischt mit Format-Logik
Besser: Middlewares validateschema(schema, property) 1. return middleware 2. middleware(req, res, next) 1. validieren mit daten aus der Property vom Request 2. Errorhandler, andernfalls Requesthandler Parameter: Schema und Property des Requests (Body, Query, Params) Alternativ Websocket-Message Middleware
Mit JOI den Server schützen Input-Müll frühzeitig erkennen und Request nicht weiter bearbeiten
[Umfrage] [Developer-Experience] [Wo & wofür werden Schemas definiert?] [Updateverfahren: Wie oft muss ich definieren? Wie oft updaten?] - Extra Rechenleistung - Automatisierung - Duplizierte Typdefinition an mehreren Orten - JOI - Mongoose - [Datenbank] [Pause?] Was geschrieben wird, aber nicht wer schreibt.
Beispiel-Request-Body [Viele Wege führen nach Rom] Was ist falsch an dieser Art der Authentisierung? -> myId könnte alles sein, selbst wenn es formal richtig ist Andere Art von Sicherheit, nicht technisch Insb. rechtlich wichtig nachzuweisen, wer wann wo was macht
Authentisierung richtig gemacht Richtig: Nicht im Request Body, sondern im Authorization Header [Zitat über Nutzung des Authorization Headers] Schema: Authorization: <auth-scheme> <authorization-parameters> Heute 2 Authentisierungsschemata: Basic & Bearer
Basic Authorization Authorization: Basic <base64('<username>:<password>')> WWW-Authenticate: Browser-Alert mit Nutzername und Passwort [Beispiel https://www.hdm-stuttgart.de/intranet] [Zeigen mit https://www.base64encode.org/]
[Warnung] Zeigen mit https://www.base64decode.org/ bWF4MTIzOnN1cGVyU3Rhcmtlc1Bhc3N3b3J0MTIz Passwort und Nutzername sind einfach dekodierbar
Bearer Authorization Authorization: Bearer <Token> - Token ist frei definierbar - Bearer bedeutet Träger, Aufbewahrer -> Ringebearer aus Herr der Ringe - Bearer Authentisierung erfordert, dass der Token vom Client nicht verändert werden kann Eine Methode, die diese Anforderungen erfüllt, sind JSON Web Tokens [Pause?]
Intermezzo über Ids Token-Idee Nutzer-Id, welche Probleme damit auftreten - Youtube: 2 Gruppen an 5 alphanumerischen Zeichen - Instagram Reels: 10 alphanumerische Zeichen - Instagram Stories & Twitter Posts: 19 numerische Zeichen
Anekdote: Youtube-Video-Ids - Zu viele Videos - Nächstes Video ist nächste Id, ggf. nicht öffentlich - Hier 36^10 Varianten = 3 * 10^15 = 3 Billiarden - Wahrscheinlichkeit 800 Mio. / 3 Billiarden = 2 / 10 Mio. = 0,00026%
Hier kommen JWTs ins Spiel Verschlüsselte Tokens zur Authentifizierung von JSON-Daten 2 Features: 1. Datenverschlüsselung: Daten sind nicht lesbar 2. Datenintegrität: Daten werden nicht verändert. Durch Signatur sichergestellt. Wir schauen uns an, wie JWTs funnktionieren.
3 Teile: 1. Header: In der Regel kurz, Metainformationen 2. Payload: Daten, die ich verschlüsseln will, z.B. Authentisierungsinformationen 3. Signature: Integrität von Header und Payload Im Detail vorgestellt ->
2 Teile im JSON: 1. Typ, hier meistens JWT 2. Algorithmus, zum Beispiel HMAC SHA256 oder RSA, für die Signatur JSON-String wird Base64Url enkodiert.
JSON ist theoretisch beliebig aufbaubar - Properties im Payload werden Claim genannt - Registrierte, sog. Public Claims, sind sub, iss, exp, aud, iat, jti, nbf - Private Claims immernoch erlaubt JSON-String wird Base64Url enkodiert.
Signatur ist Base64-encoded Header und Payload, HMAC-verschlüsselt - HMAC: hash-base message auth code - Hash-Funktionen sind one-way, man kann also nur nachweisen, dann der Body nicht verändert wurde - Deshalb muss Header und Payload trotzdem unverschlüsselt versandt werden. [Exkurs: Hash-Funktionen] -> Secret müsste dem Client bekannt sein, praktisch nicht möglich -> https://jwt.io/#debugger-io [Rumspielen]
- Base64 ist dekodierbar, also Payload lesbar und veränderbar - Aber Änderungen sind nachweisbar, weil die Signatur dann nicht mehr zum Payload und Header passt - Änderungen sind nicht reversibel -> Also keine sensiblen Daten in JWTs verpacken!
RSA-Verschlüsselung könnte Base64-Enkodierung und HMAC ersetzen - nur Receiver kann Payload und Header lesen - nur Sender kann Signatur schreiben
Im Client JWT im Authorization-Header setzen 1. Token vom letzten Request gespeichert, modifiziert und/oder generiert 2. Authorization Header: 'Bearer ' + token
Im Server generieren 1. NPM-package `jsonwebtokens` 2. Random Secret generieren 3. jwt.sign(payload, secret, public_claims) 4. Im Request handler
Besser in einer Middleware 1. req.headers['authorization'] 2. token auslesen 3. nicht vorhanden: 401 4. andernfalls verifizieren 5. nicht valide: 403 6. andernfalls nächster Requesthandler
Jetzt dazugekommen: 1. Inputvalidierung 2. Authentisierung