Schutz vor gängigen Webangriffen

Wir haben jetzt die Transportschicht abgesichert und die Authentifizierung gehärtet. Aber was passiert, wenn ein Angreifer die Webanwendung selbst ins Visier nimmt? Schauen wir uns die häufigsten Angriffstypen an und vor allem, wie du dich dagegen verteidigst.

Die Angriffstypen selbst wurden in Modul B2 detailliert behandelt. Hier konzentrieren wir uns auf die Verteidigung.

Stell dir vor, ein Angreifer tippt in ein Suchfeld deiner Webanwendung keinen Suchbegriff ein, sondern einen manipulierten SQL-Befehl und bekommt plötzlich Zugriff auf die gesamte Datenbank. Genau das ist SQL-Injection, und es gehört bis heute zu den häufigsten Angriffen auf Webanwendungen.

Der Angreifer schleust SQL-Befehle über Benutzereingaben ein und manipuliert Datenbankabfragen.

Ein einfaches Beispiel: Ein Angreifer gibt als Benutzernamen folgendes ein: 

' OR 1=1 --

Die Abfrage wird in der Webanwendungslogik zu:

SELECT * FROM users WHERE username = '' OR 1=1 --' AND password = ''

Kurze Erläuterung zur Wiederholung: OR 1=1 ist immer wahr, -- kommentiert den Rest aus. Damit ist der Angreifer ohne Passwort eingeloggt, da die SQL-Abfrage insgesamt immer wahr ist und das Passwort nicht mehr geprüft wird.

Wie schützt du dich dagegen? Die wichtigste Gegenmaßnahme sind sogenannte Prepared Statements (auch parametrisierte Abfragen genannt). Dabei werden SQL-Befehl und Benutzerdaten strikt getrennt. Die Datenbank behandelt die Eingabe dann immer als Datenwert, nie als ausführbaren Befehl. Schauen wir uns den Unterschied an:

Unsicher mit String-Verkettung (anfällig für SQL-Injection):

query = "SELECT * FROM users WHERE username = '" + eingabe + "'"

Hier wird die Benutzereingabe direkt in den SQL-Befehl eingebaut. Der Angreifer kann die Abfrage manipulieren, weil die Datenbank nicht unterscheiden kann, wo der Datenwert aufhört und der SQL-Befehl anfängt.

Sicher mit Prepared Statement:

query = "SELECT * FROM users WHERE username = ?"

db.execute(query, [eingabe])

Der SQL-Befehl wird als feste Vorlage mit einem Platzhalter (?) definiert. Die Benutzereingabe wird separat als Parameter übergeben. Die Datenbank behandelt sie dadurch als Wert und nicht als Bestandteil des SQL-Befehls. Selbst wenn die Eingabe SQL-Syntax enthält, verändert sie daher nicht die Struktur der Abfrage.

💡 Prepared Statements sind kein „Nice-to-have“, sondern ein Muss. Jede Datenbankabfrage, die Benutzereingaben enthält, muss parametrisiert sein.

Ein weiterer Schutz ist die Verwendung eines ORM (Object-Relational Mapping). Ein ORM bildet Datenbanktabellen und deren Datensätze auf Objekte der Programmiersprache ab. Das heißt, dass die Daten der relationalen Datenbank für die Verwendung in der Programmiersprache in passende Objekte übertragen werden. Eine Tabelle kann beispielsweise durch eine Klasse und ein einzelner Datensatz durch ein Objekt dieser Klasse repräsentiert werden.

Entwickler müssen dadurch viele SQL-Abfragen nicht mehr selbst als Strings zusammensetzen. Stattdessen verwenden sie Methoden des ORM, die Benutzereingaben als Parameter an die Datenbank übergeben.

Ein Beispiel ist das PHP-Framework Laravel. Ohne ORM könnte eine Datenbankabfrage unsicher so zusammengesetzt werden:

$email = $_POST["email"];
$password = $_POST["password"];

$sql = "SELECT * FROM users WHERE email = '$email'";

Der Inhalt von $email wird hier direkt Bestandteil des SQL-Strings. Eine entsprechend manipulierte E-Mail-Adresse kann daher zu einer SQL-Injection führen. Das Passwort wird anschließend benötigt, um es mit dem in der Datenbank gespeicherten Passwort-Hash zu vergleichen.

Mit Laravels ORM Eloquent lässt sich die Suche nach dem Benutzer dagegen so formulieren:

$email = $_POST["email"]; 
$password = $_POST["password"]; 

$user = User::where('email', $email)->first(); 

if ($user && Hash::check($password, $user->password)) { 
   // Login erfolgreich 
}

user repräsentiert das Benutzermodell. where() behandelt $email als gebundenen Parameter statt als Bestandteil des SQL-Codes. Mit ->first(); wird der erste gefundene Datensatz zurückgegeben und in user gespeichert. Hash::check() prüft anschließend das eingegebene Passwort gegen den gespeicherten Passwort-Hash. SQL-Code und Benutzereingaben bleiben damit voneinander getrennt.

Ein ORM ist allerdings kein automatischer Schutz vor jeder SQL-Injection. Werden Raw-SQL-Funktionen verwendet und Benutzereingaben dort direkt in SQL-Strings eingebaut, kann die Schwachstelle erneut entstehen.

Zusätzlich solltest du den Datenbankbenutzer immer nach dem Least-Privilege-Prinzip konfigurieren:

  • Die Webanwendung bekommt nur die Rechte, die sie tatsächlich braucht.
  • Das sind typischerweise SELECT, INSERT, UPDATE und DELETE auf die benötigten Tabellen.
  • Kein DROP, kein CREATE, kein Zugriff auf andere Datenbanken. Bei NovaHealth hätte das Patientenportal z.B. keinen Zugriff auf die Personaldatenbank.

Das verhindert SQL-Injection nicht, begrenzt aber den Schaden. Selbst wenn ein Angreifer eine Lücke findet, kann er die Datenbank nicht löschen oder auf andere Bereiche zugreifen.

Du hast XSS ja schon im Kursmodul B2 kennengelernt. Für die Einstiegsszenarien taucht typischerweise plötzlich ein Popup-Fenster im Browser auf, aber das ist natürlich nur ein sehr einfaches Beispiel:

XSS bedeutet: der Angreifer schleust JavaScript-Code ein, der im Browser anderer Benutzer ausgeführt wird.

„Wie kann denn ein Angreifer JavaScript in unsere Website einschleusen?“

„Das kann passieren, wenn die Anwendung Benutzereingaben ungefiltert auf der Seite anzeigt. Stell dir vor, jemand schreibt in ein Kommentarfeld statt eines Textes ein HTML- <script>-Tag und beim nächsten Besucher wird dieses Skript im Browser ausgeführt, weil der Server die Ausgabe nicht korrekt filtert und der Browser dies als gültigen Code erkennt.“

Was sind mögliche Schutzmaßnahmen?

Die erste und wichtigste Maßnahme ist Output Encoding. Das Prinzip ist folgendes: Jede Benutzereingabe wird vor der Ausgabe auf der Webseite so umgewandelt, dass der Browser sie als Text darstellt und nicht als Code ausführt. Konkret wird z.B. <script> zu &lt;script&gt; Das heißt: der Browser zeigt den Text an, führt ihn aber nicht aus. Auf der Oberfläche stehen zwar die Zeichen < und >, ein Blick in den HTML-Quellcode zeigt jedoch &lt; und &gt;. Der Browser stellt diese HTML-Entities wieder als < und > dar. Auch andere Zeichen werden abhängig vom jeweiligen HTML-Kontext codiert, beispielsweise & als &amp;, " als &quot; oder ' als &#39;.

💡 Die meisten modernen Web-Frameworks machen Output Encoding automatisch, solange keine Ausnahmen konfiguriert sind.

Die zweite Schutzschicht ist die Content Security Policy (CSP). Das ist ein Security-HTTP-Header gegen XSS. Eine CSP legt fest, aus welchen Quellen der Browser Ressourcen laden darf: Skripte, Stylesheets, Bilder, Fonts, iFrames. Alles, was nicht explizit erlaubt ist, wird blockiert.

Beispiel:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'

Was bedeuten die einzelnen Direktiven?

  • default-src 'self' – Standardmäßig dürfen Ressourcen nur von der eigenen Domain geladen werden
  • script-src 'self' – JavaScript darf nur von der eigenen Domain ausgeführt werden (das ist die entscheidende XSS-Schutzmaßnahme)
  • style-src 'self' – CSS-Stylesheets nur von der eigenen Domain
  • img-src 'self' data: – Bilder von der eigenen Domain und eingebettete Data-URIs erlaubt
  • frame-ancestors 'none' – Die Seite darf von niemandem in einem iFrame eingebettet werden (Clickjacking-Schutz)

Ein eingeschleustes <script src="https://evil.com/steal.js"> wird vom Browser blockiert, weil evil.com nicht in script-src erlaubt ist. Selbst wenn ein Angreifer es schafft, ein Skript in den HTML-Code einzuschleusen, kann es ohne passende CSP-Erlaubnis nicht ausgeführt werden.

Auf der Website https://csp-evaluator.withgoogle.com/ lässt sich eine CSP prüfen und es werden Empfehlungen zur Absicherung gezeigt.

⚠️ CSP richtig zu konfigurieren erfordert Sorgfalt. Eine zu lockere Policy (z.B. script-src 'unsafe-inline') macht den Schutz wirkungslos, weil Inline-Skripte und damit auch eingeschleuster Code erlaubt werden. Eine zu strenge Policy kann die eigene Anwendung kaputt machen, weil auch gewollte Skripte blockiert werden. In der Praxis empfiehlt sich ein schrittweises Vorgehen: Mit Content-Security-Policy-Report-Only kannst du eine Policy erst im Beobachtungsmodus testen, damit der Browser Verstöße meldet, aber erstmal nichts blockiert. So siehst du, was kaputt gehen würde, bevor du die Policy scharf schaltest.

Die dritte Maßnahme betrifft die Session-Cookies: Das Flag HttpOnly macht Cookies per JavaScript unzugreifbar. Warum ist das wichtig? Eines der Hauptziele von XSS-Angriffen ist es, den Session-Cookie des Opfers zu stehlen (Methode document.cookie) und an den Angreifer zu senden. Mit HttpOnly ist das Cookie für JavaScript unsichtbar und selbst bei einem erfolgreichem XSS kann der Angreifer den Session-Cookie nicht auslesen. Das ist kein XSS-Schutz (der Angreifer kann andere schädliche Aktionen im Browser ausführen), aber es schützt zumindest den Cookie.

Der Angreifer bettet die Zielseite in einem unsichtbaren <iframe> ein und legt eine eigene Seite darüber. Der Benutzer denkt, er klickt auf einen harmlosen Button, klickt aber tatsächlich auf die versteckte Seite und löst ungewollte Aktionen aus.

Das Bild hier zeigt den „Click Bandit“ der Burp Suite, welcher halbtransparent über der Website liegt und das iFrame automatisch richtig positioniert. Das Beispiel im Bild mit dem Anmelde-Button dient nur zur Veranschaulichung, gefährlicher wäre ein Button wie „Sicher, dass Sie die Datenbank löschen wollen“ und dort ein „Klicke hier für süße Katzenbilder“ drüber liegt. Das Beispiel zeigt, dass es dafür spezialisierte Tools gibt und ein solcher Angriff nicht unrealistisch ist.

Der Schutz ist hier vergleichsweise einfach: Der Server muss dem Browser mitteilen, dass die Seite nicht in einem iFrame eingebettet werden darf. Dafür gibt es zwei Header: X-Frame-Options: DENY verbietet das Einbetten komplett, SAMEORIGIN erlaubt es nur von der eigenen Domain. Die modernere Variante ist die CSP-Direktive frame-ancestors 'none', die wir im CSP-Beispiel oben bereits gesehen haben. Am besten werden beide Header gesetzt: X-Frame-Options für ältere Browser, frame-ancestors für moderne Browser.

Der Angreifer bringt den Browser eines angemeldeten Benutzers dazu, ungewollte Aktionen auszuführen. Ein Beispiel: Melina ist im NovaHealth-Verwaltungsportal eingeloggt. Sie erhält eine E-Mail mit einem Link zu einer scheinbar harmlosen Website. Diese Website enthält ein verstecktes Formular, das im Hintergrund eine Anfrage an portal.novahealth-solutions.de/admin/delete-user sendet. Weil Melinas Browser den Session-Cookie automatisch mitschickt, wird die Anfrage mit ihren Berechtigungen ausgeführt, ohne dass sie es merkt.

Gegen CSRF gibt es effektive Schutzmaßnahmen:

  • Anti-CSRF-Token: Jedes Formular enthält ein einmaliges, zufälliges Token, das der Server bei der Formularanzeige generiert. Bei jedem Request prüft der Server, ob das Token gültig ist. Ein Angreifer auf einer fremden Website kennt das Token nicht und kann es nicht in sein verstecktes Formular einbauen.
  • SameSite-Cookie-Attribut: SameSite=Strict oder SameSite=Lax verhindert, dass der Browser Cookies bei Cross-Site-Requests mitsendet. Damit wird der Angriff wirkungslos, weil die Anfrage von der fremden Website ohne Session-Cookie ankommt.

In den obigen Abschnitten hast du bereits die wichtigsten Security-Header in Aktion gesehen: CSP gegen XSS, X-Frame-Options und frame-ancestors gegen Clickjacking, HttpOnly und SameSite als Cookie-Schutz gegen XSS und CSRF. Hier fassen wir alle Header und Flags zusammen und ergänzen zwei weitere, die bisher nicht behandelt wurden.

„Warum muss der Server dem Browser sagen, wie er sich verhalten soll? Kann der Browser das nicht selbst?“

„Browser sind von Haus aus sehr tolerant. Sie sind historisch so programmiert, dass sie versuchen alles darzustellen, was sie bekommen. Das ist bequem, aber aus Sicherheitssicht problematisch. Die Security-Header sagen dem Browser: ‚Sei hier bitte strenger.’“

Wenn ein Server eine Datei ohne klaren Content-Type ausliefert (oder mit einem falschen), kann der Browser versuchen, den Dateityp anhand des Inhalts selbst zu bestimmen. Das nennt man MIME-Type-Sniffing. Das kann zum Sicherheitsproblem werden, wenn ein Browser beispielsweise eine als harmlose Datei deklarierte Ressource trotzdem als ausführbaren Inhalt interpretiert.

Der Header X-Content-Type-Options: nosniff weist den Browser an, sich bei sicherheitsrelevanten Ressourcentypen an den vom Server angegebenen MIME-Type zu halten. So wird beispielsweise ein Skript blockiert, wenn es nicht mit einem zulässigen JavaScript-MIME-Type ausgeliefert wird. Der Header kennt nur den Wert nosniff.

Wenn ein Benutzer einen Link klickt, der zu einer Seite einer anderen Webpräsenz führt, sendet der Browser standardmäßig die vollständige URL der Herkunftsseite als Referrer-Header mit. Das kann problematisch sein: Stell dir vor, ein NovaHealth-Mitarbeiter klickt aus dem internen Patientenportal (portal.novahealth-solutions.de/patient/12345/befund) auf einen externen Link.

Ohne Referrer-Policy sieht die Zielseite die vollständige URL, auch inklusive der Patienten-ID. Mit Referrer-Policy: strict-origin-when-cross-origin wird bei externen Links nur die Domain übermittelt (portal.novahealth-solutions.de), nicht der vollständige Pfad.

HeaderFunktionEmpfohlener Wert
Content-Security-PolicyKontrolliert, welche Ressourcen geladen werden dürfen (XSS-Schutz)default-src 'self'; script-src 'self' (Minimum)
X-Frame-OptionsVerhindert Einbetten in iFrames (Clickjacking-Schutz)DENY oder SAMEORIGIN
X-Content-Type-OptionsVerhindert MIME-Type-Sniffingnosniff
Referrer-PolicyBegrenzt URL-Informationen bei externen Linksstrict-origin-when-cross-origin
Strict-Transport-SecurityErzwingt HTTPS (siehe Abschnitt Transportsicherheit)max-age=31536000; includeSubDomains; preload

Cookies sind das häufigste Ziel bei Angriffen auf Webanwendungen, weil sie die Session-ID enthalten und damit den Schlüssel zur authentifizierten Sitzung des Benutzers. Drei Flags sind entscheidend für die Absicherung:

Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Strict; Path=/

  • Secure – Das Cookie wird nur über HTTPS gesendet, nie über unverschlüsseltes HTTP. Ohne dieses Flag könnte ein Angreifer in einem öffentlichen WLAN das Cookie im Klartext mitlesen.
  • HttpOnly – Das Cookie ist per JavaScript nicht zugreifbar (wie oben bei XSS erklärt). Schützt die Session-ID vor Diebstahl durch eingeschleusten Code.
  • SameSite=Strict – Das Cookie wird bei Cross-Site-Requests nicht mitgesendet (wie oben bei CSRF erklärt). Lax ist eine etwas lockerere Variante, die Cookies bei normaler Navigation (z.B. Klick auf einen Link) noch mitsendet, aber bei POST-Requests von fremden Seiten blockiert.
  • Path=/ – Das Cookie gilt für die gesamte Website. Kann auf einen engeren Pfad eingeschränkt werden, wenn mehrere Anwendungen auf derselben Domain laufen.

Neben den bisher genannten Maßnahmen gibt es noch weitere Schutzmaßnahmen, die ergänzend zum Einsatz kommen sollten:

  • Input-Validierung: Alle Benutzereingaben sollten auf dem Server darauf geprüft werden, ob sie dem erwarteten Format entsprechen. Dabei sollte nach Möglichkeit Allowlisting eingesetzt werden: Statt nur bestimmte unerwünschte Zeichen oder Inhalte zu verbieten, wird festgelegt, welche Werte überhaupt zulässig sind. Bei einer Altersangabe könnten beispielsweise ausschließlich Zahlen innerhalb eines bestimmten Wertebereichs akzeptiert werden. Input-Validierung allein reicht jedoch nicht als Schutz vor Injection-Angriffen aus. Sie bildet lediglich eine zusätzliche Sicherheitsschicht.
  • Sichere Fehlerbehandlung: Fehlermeldungen sollten keine technischen Interna wie Stack Traces, SQL-Abfragen, Datenbanknamen, Dateipfade oder Versionsinformationen an den Benutzer weitergeben. Solche Informationen können einem Angreifer wertvolle Hinweise über den Aufbau der Anwendung liefern. Nach außen sollte daher nur eine generische Fehlermeldung erscheinen. Die technischen Details werden stattdessen intern protokolliert, damit Administratoren und Entwickler die Ursache nachvollziehen können.

Puh, das war einiges… Ok, dann schauen wir mal, ob du die Verteidigungsstrategien gegen Webangriffe draufhast.

Ordne jeder Schutzmaßnahme den primären Angriff zu, gegen den sie schützt:

Nach oben scrollen