Sicherer Code-Review: 8 Best Practices für Sicherheits-Code-Reviews
20. April 2020
0 Min. Lesezeit
„Es ist immer eine gute Idee, den Code, den Sie überprüfen, auf Sicherheitsprobleme im Code zu prüfen. Falls Sie nicht wissen, worauf Sie achten sollten, hilft Ihnen diese praktische Checkliste bei Ihren nächsten Code-Reviews! Code-Reviews sind nicht leicht richtig durchzuführen. Besonders dann nicht, wenn Sie sich nicht ganz sicher sind, nach welchen Fehlern Sie suchen sollten! Der DevSecOps-Ansatz verlagert Sicherheitstests nach links, sodass Sicherheitslücken bereits in den Design-, Entwicklungs- oder CI/CD-Phasen des Workflows gefunden und behoben werden können. Es ist immer eine gute Idee, den Code, den Sie überprüfen, auf Sicherheitsprobleme im Code zu prüfen. Falls Sie nicht wissen, worauf Sie achten sollten, hilft Ihnen diese praktische Checkliste bei Ihren nächsten Code-Reviews!“
Beachten Sie bei der Code-Überprüfung, dass nicht jeder Code gleich ist! Denken Sie auch daran, was hinter dem überprüften Code steht, und damit an die Daten und Assets, die Sie schützen möchten. Dieses Hintergrundwissen lässt sich nicht ohne Weiteres in eine Checkliste aufnehmen. Die Tipps in diesem Spickzettel helfen Ihnen jedoch zusammen mit Ihrem Fachwissen dabei, zu entscheiden, worauf Sie mehr Zeit verwenden sollten und wo ein höheres Risiko sowie unterschiedliche Angriffsarten zu erwarten sind. Eine gute Möglichkeit, Ihre größten Risikobereiche zu ermitteln, besteht darin, Angriffsbäume zu erstellen. Sie zeigen Ihnen, worauf Sie Ihre Bemühungen zuerst und besonders konzentrieren sollten.
Beginnen wir also mit unserer Liste mit 8 Tipps für sichere Code-Reviews, auf die Sie bei zukünftigen Pull Requests achten sollten!
1. Alle Eingaben bereinigen und validieren
Moderne Webanwendungen müssen mit allen möglichen Eingaben von Drittanbietern umgehen. Direkte Eingaben von Endnutzern im Browser sind beispielsweise ein offensichtlicher Fall. Als Entwickler wissen wir alle, dass Nutzer unerwartete Inhalte eingeben, wenn dies möglich ist. Direkte Nutzereingaben angemessen zu validieren und zu bereinigen, gilt als zentrale Best Practice, um Anwendungen vor Content-Injection zu schützen. Direkte Nutzereingaben sind jedoch bei Weitem nicht das Einzige, was Sie prüfen sollten. Grundsätzlich sollten alle Eingaben, die von außerhalb der Systemgrenzen kommen, als potenziell schädlich betrachtet und behandelt werden. Denken Sie beispielsweise an:
Datenfeeds
Dateien
Ereignisse – etwa in einem ereignisgesteuerten System, beispielsweise bei der engen Zusammenarbeit mit Plattformen wie Functions as a Service
Datenantworten anderer Systeme
Cookies
Darüber hinaus können auch Eingaben schädlich sein, die auf den ersten Blick unter Ihrer Kontrolle zu stehen scheinen. Denken Sie daran: Kann ein böswilliger Nutzer direkt auf Ihre Datenbank zugreifen, kann er beispielsweise über eine Hintertür Schadcode einschleusen, der in Ihrem System ausgeführt wird. Dasselbe gilt für:
Kommandozeilenparameter
Umgebungsvariablen
Systemeigenschaften
Datenspeicher
Alle Eingaben sollten validiert und bereinigt werden – auch solche, die scheinbar von Ihnen kontrolliert werden. Prüfen Sie, ob die Eingabe plausibel ist. Das Typsystem einer typsicheren Programmiersprache kann dabei sehr hilfreich sein. Überprüfen Sie außerdem Format, Wertebereich, Größe, Dateityp und Dateinamen. Gehen Sie von nichts einfach aus. Nutzereingaben sollten vor dem Speichern oder der Verwendung bereinigt werden, vorzugsweise mit einer gut geprüften Bibliothek.
2. Secrets niemals als Code oder Konfiguration speichern
Anmeldedaten, Tokens oder andere Secrets lassen sich allzu leicht als Variablen oder Konstanten speichern – schließlich testen wir ja nur, ob alles funktioniert. Doch genauso leicht landet dieser Code in Ihrem Code-Repository, weil Sie vergessen haben, ihn zu entfernen. Achten Sie darauf, dass der von Ihnen überprüfte Code keine vertraulichen Informationen enthält. Wenn Sie ein Git-basiertes Code-Repository verwenden, gibt es viele nützliche Tools wie git-secrets, die Ihre Commits statisch analysieren können. Über einen Git-Pre-Commit-Hook stellen sie sicher, dass Sie keine Passwörter oder vertraulichen Informationen in Ihr Repository übertragen. Commits werden abgelehnt, wenn das Tool eines der konfigurierten regulären Ausdrücke erkennt, die auf unsachgemäß gespeicherte vertrauliche Informationen hinweisen. Das kann Pushes ein wenig verlangsamen, ist es aber allemal wert.
Teamweite Regeln, die verhindern, dass Anmeldedaten als Code gespeichert werden, sind eine gute Möglichkeit, problematische Aktionen im bestehenden Entwickler-Workflow zu überwachen. Verwenden Sie Tools wie Vault, um Ihre Secrets in der Produktionsumgebung zu verwalten. Ziehen Sie außerdem eine Toolchain für Identitäts- und Nutzermanagement in Betracht, zum Beispiel Keycloak (das derzeit von mehreren Entwicklern bei Red Hat betreut wird) oder vergleichbare Lösungen.
Es gibt viele Möglichkeiten, Anmeldedaten von vornherein aus Ihrem Repository herauszuhalten. Am besten setzen Sie so viele davon wie möglich um. Trotzdem können vertrauliche Informationen immer einmal durchsickern. Prüfen Sie Ihre Repositories daher regelmäßig mit Tools wie GitRob oder truffleHog. Beide durchsuchen Ihre Codebasis per Musterabgleich nach vertraulichen Informationen.
3. Auf neue Sicherheitslücken in Open-Source-Abhängigkeiten von Drittanbietern testen
Die moderne Anwendungsentwicklung ist stark von Bibliotheken von Drittanbietern abhängig. Paketmanager wie npm, Maven, Gradle, PyPI oder vergleichbare Tools bieten einfachen Zugriff auf öffentlich verfügbare Bibliotheken und Frameworks. Als Entwickler möchten wir uns auf bestimmte Geschäftslogik konzentrieren, statt Standardfunktionen selbst zu erstellen. Frameworks und Bibliotheken die Arbeit übernehmen zu lassen, liegt daher nahe.
Möglicherweise wissen Sie nicht genau, wie viele direkte Abhängigkeiten Ihre Anwendung verwendet. In einem durchschnittlichen Projekt kann Ihr eigener Code nur 1 % ausmachen – der Rest besteht aus importierten Bibliotheken und Frameworks. Ein großer Teil des produktiv eingesetzten Codes stammt also nicht von uns, obwohl wir stark davon abhängig sind. Wahrscheinlich wissen Sie auch nicht genau, wie viele transitive Abhängigkeiten Ihre Anwendung verwendet. Größere Frameworks hängen heute von anderen Bibliotheken ab, die wiederum weitere Bibliotheken einbinden. Wenn Sie eine einzelne Bibliothek oder ein Framework hinzufügen, kommen oft mindestens ein Dutzend weitere Bibliotheken und/oder Frameworks hinzu, von denen Sie nicht immer wissen. So machen Abhängigkeiten den Großteil Ihrer Anwendung aus. Angreifer nehmen Open-Source-Abhängigkeiten zunehmend ins Visier, denn durch ihre Wiederverwendung können sie mit einem Angriff viele Opfer erreichen. Deshalb müssen Sie sicherstellen, dass der gesamte Abhängigkeitsbaum Ihrer Anwendung keine bekannten Sicherheitslücken enthält.
Nehmen wir Snyk als Beispiel. Snyk analysiert Ihr Projekt statisch, um anfällige Abhängigkeiten zu finden, die Sie möglicherweise verwenden, und hilft Ihnen, diese zu beheben. Sie können Ihre Repositories über die Snyk-Benutzeroberfläche testen, um Probleme zu finden. Außerdem können Sie Pull Requests testen und den Test fehlschlagen lassen, wenn dadurch eine neue Sicherheitslücke eingeführt wird. Automatisch erstellte PRs mit Korrekturen sind ebenfalls möglich.
Je nach bevorzugter Arbeitsweise können Sie Ihr Repository mit der Snyk-Benutzeroberfläche verbinden oder das Projekt auf Ihrem lokalen Rechner mit der CLI (siehe CLI-Spickzettel), einer Integration in Ihr Build-System oder einem IDE-Plugin scannen. Vom lokalen Rechner der Entwickler ganz links bis zu Ihrem Produktionssystem ganz rechts und auf jedem Schritt dazwischen sollten Sie Ihre Abhängigkeiten automatisch analysieren, um schnell Feedback zu erhalten.
4. Sichere Authentifizierung durchsetzen
Bei der Authentifizierung wird überprüft, ob ein Nutzer, ein Dienst oder eine andere Entität – intern oder extern – tatsächlich die Person oder Instanz ist, für die sie sich ausgibt. Das kann so einfach sein wie ein Nutzer, der Ihnen seine Anmeldedaten übermittelt, oder ein Server, der Ihnen sein TLS-Zertifikat vorlegt, damit Sie bestätigen können, dass es sich tatsächlich um den angegebenen Server handelt. Die Authentifizierung legt nicht fest, was ein Nutzer oder Dienst tun darf, sondern bestätigt lediglich dessen Identität. Sehen wir uns einige Best Practices für die Authentifizierung an:
Gehen Sie davon aus, dass die Identität nicht der angegebenen entspricht.
Gehen Sie davon aus, dass ein Nutzer oder Dienst nicht die angegebene Identität hat, bis die entsprechenden Anmeldedaten dies belegen. Es ist am sichersten, zunächst anzunehmen, dass der Nutzer oder Dienst keinen Zugriff auf Ihre Daten haben sollte. Sorgen Sie dafür, dass Ihr Code diesem Grundsatz folgt.
Hohe Passwortkomplexität erzwingen
Bei Nutzern sollten Sie bei der Wahl der Benutzernamen großzügiger sein, insbesondere bei E-Mail-Adressen. Es bringt beispielsweise wenig, zwischen Patch@snyk.io und patch@snyk.io zu unterscheiden. Wichtig ist dagegen, eine hohe Passwortkomplexität durchzusetzen (mindestens ein Großbuchstabe, ein Kleinbuchstabe (a–z), eine Ziffer (0–9) und ein Sonderzeichen) sowie eine Mindestlänge vorzugeben (NIST SP800-132). Ich weiß, es ist schwierig, sich all diese zufälligen – oder auch nicht zufälligen – langen und komplexen Passwörter zu merken. Aber wir schreiben das Jahr 2020, und Passwortmanager sind da, um Ihnen zu helfen!
Vor sensiblen Vorgängen erneut authentifizieren
Wenn Nutzer vor einer Geldüberweisung oder anderen sensiblen Aktionen erneut ihre Anmeldedaten eingeben müssen, lassen sich potenzielle Angriffe durch Cross-Site-Request-Forgery (CSRF) und Session-Hijacking abmildern. Ein Angreifer könnte solche sensiblen Aktionen ausführen, ohne jemals die Anmeldedaten des Nutzers eingegeben zu haben. Diese Sicherheitsmaßnahme ist für Ihre Nutzer zwar unbequem, kann sie langfristig jedoch schützen.
TLS-Client-Authentifizierung
Bei der TLS-Client-Authentifizierung, auch Zwei-Wege-TLS-Authentifizierung genannt, müssen sich sowohl der Browser als auch der Server authentifizieren. Dazu senden beide beim TLS-Handshake ihre TLS-Zertifikate. Ein Nutzer oder Dienst erhält dafür ein Client-Zertifikat vom Server und legt es bei späteren Interaktionen vor. Bei Verwendung eines Browsers muss der Nutzer das Zertifikat möglicherweise installieren.
5. Das Prinzip der geringsten Berechtigungen durchsetzen
Auf die Authentifizierung folgt die Autorisierung. Die Begriffe klingen ähnlich, bedeuten aber etwas Unterschiedliches. Wie in Punkt 4 beschrieben, bestätigt die Authentifizierung, dass ein Nutzer oder Dienst tatsächlich die angegebene Identität hat. Die Autorisierung geht darüber hinaus und stellt sicher, dass die betreffende Person oder der betreffende Dienst die gewünschte Aufgabe oder Aktion ausführen darf. Wir wissen, dass wir dies überprüfen und sicherstellen müssen, dass Nutzer, Dienste oder Prozesse in einer Rolle ausgeführt werden oder existieren, die zu einer solchen Aktion berechtigt ist. Beim Programmieren ist es jedoch oft allzu leicht, mehr Zugriff zu gewähren als tatsächlich erforderlich.
Das Prinzip der geringsten Berechtigungen besagt, dass jedes Modul – etwa ein Prozess, Nutzer oder Programm, je nach Kontext – nur auf die Informationen und Ressourcen zugreifen darf, die für seinen legitimen Zweck erforderlich sind. Gewähren Sie Personen und Prozessen also nur die Mindestberechtigungen, die sie zum Erreichen ihres Ziels benötigen.
Eine gute Möglichkeit, dies zu überprüfen, besteht darin, gezielte automatische Unit- und Integrationstests zu schreiben. Diese sollten nicht nur den Erfolgsfall, sondern vor allem auch sicherheitsrelevante Fehlerfälle abdecken. Bei den Tests sollte die Authentifizierung erfolgreich sein, anschließend sollten jedoch Vorgänge versucht werden, für die keine Berechtigung besteht. Ergänzen Sie solche Tests immer dann, wenn Sie die Rollen ändern, unter denen Ihre Anwendung ausgeführt wird, oder neue Ressourcen einführen, für deren Nutzung eine bestimmte Rolle erforderlich ist.
6. Sensible Daten sorgfältig behandeln
Die Offenlegung sensibler Daten – etwa personenbezogener Informationen oder Kreditkartennummern Ihrer Kunden – kann großen Schaden anrichten. Doch auch weniger offensichtliche Fälle können ebenso schädlich sein. So kann beispielsweise die Offenlegung eindeutiger Kennungen in Ihrem System problematisch sein, wenn sich damit in einem weiteren Aufruf zusätzliche Daten abrufen lassen.
Prüfen Sie zunächst sorgfältig das Design Ihrer Anwendung und stellen Sie fest, ob Sie die Daten wirklich benötigen. Sorgen Sie außerdem dafür, dass Sie keine sensiblen Daten offenlegen, etwa durch Protokollierung, Autovervollständigung oder Datenübertragung.
Sensible Daten speichern
Wenn Sie sensible Daten wie personenbezogene Daten (PII) oder Finanzdaten speichern müssen, achten Sie auf eine angemessene Verschlüsselung. Wahrscheinlich möchten und müssen Sie die DSGVO einhalten. Vor allem aber möchten Sie verhindern, dass die Daten Ihrer Kunden kompromittiert werden. Wenn Sie die Daten im Original wiederherstellen müssen, verwenden Sie einen starken Algorithmus für die symmetrische oder asymmetrische Verschlüsselung. Wenn Sie Passwörter speichern müssen, verwenden Sie einen starken kryptografischen Hash-Algorithmus. Entwickeln Sie auf keinen Fall eine eigene Verschlüsselung. Finden Sie heraus, welche Verschlüsselung Sie benötigen, und verwenden Sie eine bewährte Bibliothek. Verwenden Sie beispielsweise BCrypt für das Hashing von Passwörtern sowie die Verschlüsselungsalgorithmen Triple DES, RSA und AES, um Daten zu verschlüsseln, die Sie wiederherstellen müssen. Überprüfen Sie vor allem regelmäßig, ob die verwendeten Algorithmen noch sicher genug sind. Was heute noch völlig sicher ist, kann morgen bereits kompromittiert sein.
Denken Sie auch daran, dass sich sensible Daten im Arbeitsspeicher befinden können. Wenn Sie ein Passwort in Ihrem System ändern, sollten Sie eine vorübergehende Speicherung in einem unveränderlichen Datentyp vermeiden. Wenn Sie beispielsweise in Java ein String-Objekt verwenden, um Ihr Passwort im Arbeitsspeicher zu speichern, bleibt der ursprüngliche Wert dort, bis der Garbage Collector ihn entfernt, da String unveränderlich ist. In diesem Fall wäre ein Byte-Array die bessere Wahl.
Sensible Daten übertragen
Wenn Sie sensible Daten übertragen müssen, prüfen Sie, ob die Verbindung sicher ist. Sensible Daten sollten ausschließlich verschlüsselt und über TLS übertragen werden. Achten Sie außerdem darauf, dass die TLS-Version aktuell ist. Kreditkartendaten über HTTP als Query-Parameter oder im Klartext im Payload zu versenden, ist selbstverständlich keineswegs sicher.
Sitzungsdaten
Zu guter Letzt sollten Sie auch Sitzungsdaten als sensible Daten betrachten. Speichern Sie sensible Daten am besten gar nicht in Cookies, sondern verwenden Sie stattdessen eine Sitzungskennung und speichern Sie die Daten in einer serverseitig verwalteten Sitzung. Achten Sie außerdem darauf, dass Cookies verschlüsselt sind und eine angemessene Länge haben (z. B. 128 Bit). Prüfen Sie, ob Attribute wie HttpOnly, Secure, und SameSite korrekt für das Cookie festgelegt sind und ob es nach einer angemessenen Zeit abläuft. Beachten Sie: Meldet sich ein Nutzer clientseitig ab, muss die Sitzung ungültig gemacht werden, damit sie nicht anderweitig verwendet werden kann.
7. Schutz vor bekannten Angriffen
Wir können davon ausgehen, dass Angreifer unsere Anwendungen weiterhin mit vorhersehbaren, bekannten und etablierten Angriffsmethoden hacken werden. Das häufige Unwissen über gängige Schwachstellen und deren Ausnutzung führt dazu, dass sich dieselben Sicherheitsfehler in zukünftigem Code immer wiederholen. Sehen Sie sich die Schwachstellen der OWASP Top 10 an und machen Sie sich mit diesen häufigen Exploits vertraut. Hier sind einige Tipps, die Ihnen helfen, die häufigsten Arten von Schwachstellen zu vermeiden.
XSS
Bei einem Cross-Site-Scripting-Angriff (XSS) schleust ein Nutzer schädliche Daten in Ihre Anwendung ein. Diese landen in einem Kontext – etwa einem HTML-Dokument –, in dem sie ausgeführt werden können, um sensible Informationen weiterzugeben oder andere schädliche Aktivitäten auszuführen. Wenn Nutzer Daten auf Ihrer Website eingeben dürfen, die anschließend auf Ihrer Seite angezeigt werden, sollten Sie lernen, wie Sie XSS-Angriffe wirksam verhindern. Es gibt mehrere Möglichkeiten, das Risiko von XSS-Angriffen zu verringern, zum Beispiel durch HTML-Kodierung gefährlicher Zeichen wie &, <, >, „ und ‘. Werden diese Zeichen nicht zugelassen oder bereinigt, können Angreifer aus dem HTML-Tag ausbrechen und schädlichen Code ausführen. Um dies zu erschweren, können wir alle an uns übergebenen HTML-Zeichen HTML-kodieren, sodass sie nicht in einer Form dargestellt werden, die einen Ausbruch aus dem HTML-Tag ermöglicht:
HTML-Entität | HTML-Kodierung |
|---|---|
& | & |
< | < |
> | > |
“ | " |
‘ | ' |
Dasselbe erreichen wir mit Tag-Attributen, Event-Handlern oder sogar Style-Eigenschaften. Häufig werden Sanitization-Bibliotheken verwendet, um eine Positivliste der zulässigen Zeichen festzulegen. Mit bestehenden Sanitization-Bibliotheken müssen Sie außerdem keine Sicherheitsexperten sein, um alle Eventualitäten abzudecken. Das ist besonders hilfreich für weniger erfahrene Entwickler.
SQL- und NoSQL-Injection
Einer der Gründe, warum SQL-Injection für Angreifer so attraktiv ist: Sie ermöglicht ihnen den direkten Zugriff auf die Daten, an denen sie interessiert sind. Allzu oft dient ein Angriff lediglich dazu, mehr über das System zu erfahren, das sie kompromittieren wollen. Dafür müssen Angreifer häufig erst herausfinden, wo die Daten gespeichert sind und wie sie darauf zugreifen können. SQL-Injection hingegen kann Angreifern bei erfolgreicher Ausnutzung direkten Zugriff auf sensible Informationen in einer Datenbank verschaffen. Das gilt nicht nur für SQL-Datenbanken – viele NoSQL-Datenbanken lassen sich auf ähnliche Weise kompromittieren.
XSS und SQL-Injection sind sehr häufige Angriffe. Sie sollten wissen, wie sie funktionieren und wie Sie sie in Ihrem Code erkennen. Fehlende Bereinigung von Eingaben an irgendeiner Stelle im System ist ein deutliches Warnsignal für XSS. Bei SQL-Injection sollten Sie prüfen, ob Abfrageparameter verwendet werden. Dabei müssen Sie zwischen Abfrage und Parametern unterscheiden. Wenn Sie die Parameter vor ihrer Verwendung in der Abfrage an einen bestimmten Typ binden und ordnungsgemäß bereinigen, verhindern Sie solche Angriffe.
Ihre Anwendung kann für viele weitere Arten von Schwachstellen anfällig sein – manche mehr als andere. Nehmen Sie sich die Zeit herauszufinden, welche davon Ihre Anwendung am stärksten betreffen: vom Code, den Ihre Teams direkt erstellen, bis hin zu den Bibliotheken, von denen Ihre Anwendung abhängt.
8. Testen Sie Ihren Quellcode automatisch und statisch
Statische Codeanalyse ist ein leistungsstarkes Tool für Entwickler. Die sicherheitsorientierte Untergruppe dieser Tools sind Tools für Static Application Security Testing (SAST). Durch die statische Analyse des von Ihnen und Ihrem Team geschriebenen Codes zeigt ein SAST-Tool an, ob sich ein sicherheitsrelevanter Fehler in Ihren Quellcode eingeschlichen hat. Ein SAST-Tool wie Snyk Code kann Probleme wie SQL-Injections und Code-Schwachstellen erkennen.
Für die Sicherheit empfehlen wir SAST statt eines Linters. Linter können zwar bei der statischen Codeanalyse (Fehler, Bugs usw.) sehr nützlich sein, erzeugen aber viele falsch positive Ergebnisse. Da alle Linter regelbasiert arbeiten und nicht den gesamten Kontext Ihres Codes betrachten, kennzeichnen sie Ihren Code häufig als fehlerhaft oder unsicher, obwohl das nicht der Fall ist. Wenn Sie den Regelsatz jedoch feinabstimmen, kann ein Linter schwerwiegende Fehler verhindern. Am besten automatisieren Sie diese Prozesse so weit wie möglich, zum Beispiel als Teil Ihres Build-Prozesses oder beim Einreichen eines neuen Pull Requests in Ihr Repository. Snyk Code übernimmt das für Sie und integriert SAST-Funktionen direkt in Ihre Workflows.
Wenn Sie noch kein SAST-Tool verwenden, können Sie es bei manuellen Code-Reviews einsetzen – noch besser ist es, den Prozess zu automatisieren, damit offensichtliche Probleme früher erkannt werden. Sie können auch Snyks kostenlosen Code-Checker ausprobieren, um schnell einen Eindruck von der Sicherheit Ihres Codes zu erhalten.
Starten Sie mit Capture the Flag
Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.



