CodeGym /Java-Blog /Random-DE /Sicherheit in Java: Best Practices
John Squirrels
Level 41
San Francisco

Sicherheit in Java: Best Practices

Veröffentlicht in der Gruppe Random-DE
Eine der wichtigsten Kennzahlen bei Serveranwendungen ist die Sicherheit. Dies ist eine Art nichtfunktionaler Anforderung . Sicherheit in Java: Best Practices – 1Sicherheit umfasst viele Komponenten. Natürlich würde es mehr als einen Artikel erfordern, um alle bekannten Sicherheitsprinzipien und Sicherheitsmaßnahmen vollständig abzudecken, daher konzentrieren wir uns auf die wichtigsten. Eine Person, die sich mit diesem Thema auskennt, kann alle relevanten Prozesse einrichten, die Entstehung neuer Sicherheitslücken vermeiden und wird in jedem Team benötigt. Natürlich sollten Sie nicht davon ausgehen, dass Ihre Anwendung zu 100 % sicher ist, wenn Sie diese Vorgehensweisen befolgen. NEIN! Aber sicherer wird es mit ihnen auf jeden Fall sein. Lass uns gehen.

1. Bieten Sie Sicherheit auf der Ebene der Java-Sprache

Zunächst einmal beginnt die Sicherheit in Java bereits auf der Ebene der Sprachfunktionen. Was würden wir tun, wenn es keine Zugriffsmodifikatoren gäbe? Es gäbe nichts als Anarchie. Die Programmiersprache hilft uns, sicheren Code zu schreiben und nutzt auch viele implizite Sicherheitsfunktionen:
  1. Starkes Tippen. Java ist eine statisch typisierte Sprache. Dadurch ist es möglich, typbedingte Fehler zur Laufzeit abzufangen.
  2. Zugriffsmodifikatoren. Dadurch können wir den Zugriff auf Klassen, Methoden und Felder nach Bedarf anpassen.
  3. Automatische Speicherverwaltung. Dafür verfügen Java-Entwickler über einen Garbage Collector, der uns davon befreit, alles manuell konfigurieren zu müssen. Ja, manchmal treten Probleme auf.
  4. Bytecode-Überprüfung : Java wird in Bytecode kompiliert, der von der Laufzeit überprüft wird, bevor er ausgeführt wird.
Darüber hinaus gibt es die Sicherheitsempfehlungen von Oracle . Natürlich ist es nicht in hochtrabender Sprache geschrieben und man könnte beim Lesen mehrmals einschlafen, aber es lohnt sich. Besonders wichtig ist das Dokument „Secure Coding Guidelines for Java SE“ . Es gibt Ratschläge zum Schreiben von sicherem Code. Dieses Dokument vermittelt eine große Menge äußerst nützlicher Informationen. Wenn Sie die Möglichkeit haben, sollten Sie es unbedingt lesen. Um Ihr Interesse an diesem Material zu wecken, hier ein paar interessante Tipps:
  1. Vermeiden Sie die Serialisierung sicherheitsrelevanter Klassen. Durch die Serialisierung wird die Klassenschnittstelle in der serialisierten Datei verfügbar gemacht, ganz zu schweigen von den serialisierten Daten.
  2. Versuchen Sie, veränderliche Klassen für Daten zu vermeiden. Dies bietet alle Vorteile unveränderlicher Klassen (z. B. Thread-Sicherheit). Wenn Sie über ein veränderliches Objekt verfügen, kann dies zu unerwartetem Verhalten führen.
  3. Erstellen Sie Kopien der zurückgegebenen veränderlichen Objekte. Wenn eine Methode einen Verweis auf ein internes veränderbares Objekt zurückgibt, könnte der Clientcode den internen Status des Objekts ändern.
  4. Usw…
Im Grunde handelt es sich bei den Secure Coding Guidelines für Java SE um eine Sammlung von Tipps und Tricks, wie man Java-Code korrekt und sicher schreibt.

2. Beseitigen Sie SQL-Injection-Schwachstellen

Dies ist eine besondere Art von Verletzlichkeit. Das Besondere daran ist, dass es sich sowohl um eine der bekanntesten als auch um eine der häufigsten Schwachstellen handelt. Wenn Sie sich noch nie für Computersicherheit interessiert haben, wissen Sie nichts davon. Was ist SQL-Injection? Hierbei handelt es sich um einen Datenbankangriff, bei dem zusätzlicher SQL-Code dort eingefügt wird, wo er nicht erwartet wird. Angenommen, wir haben eine Methode, die einen Parameter zum Abfragen der Datenbank akzeptiert. Zum Beispiel ein Benutzername. Anfälliger Code würde etwa so aussehen:

// This method retrieves from the database all users with a certain name
public List findByFirstName(String firstName) throws SQLException {
   // Connect to the database
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);
  
   // Compose a SQL database query with our firstName
   String query = "SELECT * FROM USERS WHERE firstName = " + firstName;
  
   // Execute the query
   Statement statement = connection.createStatement();
   ResultSet result = statement.executeQuery(query);

   // Use mapToUsers to convert the ResultSet into a collection of users.
   return mapToUsers(result);
}

private List mapToUsers(ResultSet resultSet) {
   // Converts to a collection of users
}
In diesem Beispiel wird eine SQL-Abfrage vorab in einer separaten Zeile vorbereitet. Was ist also das Problem, oder? Vielleicht liegt das Problem darin, dass es besser wäre, String.format zu verwenden ? NEIN? Na, was dann? Versetzen wir uns in die Lage eines Testers und überlegen wir uns, was als Wert von firstName übergeben werden könnte . Zum Beispiel:
  1. Wir können übergeben, was erwartet wird – einen Benutzernamen. Dann gibt die Datenbank alle Benutzer mit diesem Namen zurück.
  2. Wir können eine leere Zeichenfolge übergeben. Dann werden alle Benutzer zurückgegeben.
  3. Wir können aber auch Folgendes übergeben: „'; DROP TABLE USERS;“. Und hier haben wir jetzt riesige Probleme. Diese Abfrage löscht eine Tabelle aus der Datenbank. Zusammen mit allen Daten. ALLES DAVON.
Können Sie sich vorstellen, welche Probleme dies mit sich bringen würde? Darüber hinaus können Sie schreiben, was Sie wollen. Sie können die Namen aller Benutzer ändern. Sie können ihre Adressen löschen. Das Sabotagepotenzial ist immens. Um dies zu vermeiden, müssen Sie das Einfügen einer vorgefertigten Abfrage verhindern und die Abfrage stattdessen mithilfe von Parametern formulieren. Dies sollte die einzige Möglichkeit sein, Datenbankabfragen zu erstellen. So können Sie diese Schwachstelle beseitigen. Zum Beispiel:

// This method retrieves from the database all users with a certain name
public List findByFirstName(String firstName) throws SQLException {
   // Connect to the database
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);

   // Create a parameterized query.
   String query = "SELECT * FROM USERS WHERE firstName = ?";

   // Create a prepared statement with the parameterized query
   PreparedStatement statement = connection.prepareStatement(query);
  
   // Pass the parameter's value
   statement.setString(1, firstName);

   // Execute the query
   ResultSet result = statement.executeQuery(query);

   // Use mapToUsers to convert the ResultSet into a collection of users.
   return mapToUsers(result);
}

private List mapToUsers(ResultSet resultSet) {
   // Converts to a collection of users
}
Auf diese Weise wird die Schwachstelle vermieden. Für diejenigen, die tiefer in diesen Artikel eintauchen möchten, gibt es hier ein tolles Beispiel . Woher wissen Sie, wann Sie diese Sicherheitslücke verstehen? Wenn Sie den Witz im Comic unten verstehen, haben Sie wahrscheinlich eine klare Vorstellung davon, worum es bei dieser Sicherheitslücke geht :DSicherheit in Java: Best Practices – 2

3. Abhängigkeiten scannen und auf dem neuesten Stand halten

Was bedeutet das? Wenn Sie nicht wissen, was eine Abhängigkeit ist, werde ich es Ihnen erklären. Eine Abhängigkeit ist ein JAR-Archiv mit Code, das über automatische Build-Systeme (Maven, Gradle, Ant) mit einem Projekt verbunden ist, um die Lösung einer anderen Person wiederzuverwenden. Zum Beispiel Project Lombok , das zur Laufzeit Getter, Setter usw. für uns generiert. Große Anwendungen können viele, viele Abhängigkeiten haben. Einige sind transitiv (d. h. jede Abhängigkeit kann ihre eigenen Abhängigkeiten haben usw.). Dies hat zur Folge, dass Angreifer zunehmend auf Open-Source-Abhängigkeiten achten, da diese regelmäßig genutzt werden und viele Clients dadurch Probleme haben können. Es ist wichtig sicherzustellen, dass im gesamten Abhängigkeitsbaum (ja, er sieht aus wie ein Baum) keine bekannten Schwachstellen vorhanden sind. Dafür gibt es mehrere Möglichkeiten.

Verwenden Sie Snyk zur Abhängigkeitsüberwachung

Snyk überprüft alle Projektabhängigkeiten und weist auf bekannte Schwachstellen hin. Sie können sich bei Snyk registrieren und Ihre Projekte über GitHub importieren. Sicherheit in Java: Best Practices – 3Wie Sie auf dem Bild oben sehen können, bietet Snyk außerdem die Behebung an und erstellt eine Pull-Anfrage, wenn eine Schwachstelle in einer neueren Version behoben wird. Sie können es kostenlos für Open-Source-Projekte verwenden. Projekte werden in regelmäßigen Abständen gescannt, z. B. einmal pro Woche, einmal im Monat. Ich habe alle meine öffentlichen Repositorys registriert und zum Snyk-Scan hinzugefügt (das ist nicht gefährlich, da sie bereits für alle öffentlich sind). Snyk zeigte dann das Scan-Ergebnis: Sicherheit in Java: Best Practices – 4Und nach einer Weile bereitete Snyk-Bot mehrere Pull-Requests in Projekten vor, in denen Abhängigkeiten aktualisiert werden müssen: Sicherheit in Java: Best Practices – 5Und außerdem:Sicherheit in Java: Best Practices – 6Dies ist ein großartiges Tool zum Auffinden von Schwachstellen und zum Überwachen von Updates für neue Versionen.

Verwenden Sie GitHub Security Lab

Jeder, der an GitHub arbeitet, kann die integrierten Tools nutzen. Weitere Informationen zu diesem Ansatz finden Sie in ihrem Blogbeitrag mit dem Titel „ Ankündigung des GitHub Security Lab“ . Dieses Tool ist natürlich einfacher als Snyk, aber Sie sollten es auf keinen Fall vernachlässigen. Darüber hinaus wird die Zahl der bekannten Schwachstellen weiter zunehmen, sodass sowohl Snyk als auch GitHub Security Lab weiter expandieren und sich verbessern werden.

Aktivieren Sie Sonatype DepShield

Wenn Sie GitHub zum Speichern Ihrer Repositorys verwenden, können Sie Sonatype DepShield, eine der Anwendungen im MarketPlace, zu Ihren Projekten hinzufügen. Es kann auch verwendet werden, um Projekte nach Abhängigkeiten zu durchsuchen. Wenn außerdem etwas gefunden wird, wird ein GitHub-Problem mit einer entsprechenden Beschreibung generiert, wie unten gezeigt:Sicherheit in Java: Best Practices – 7

4. Gehen Sie sorgfältig mit vertraulichen Daten um

Alternativ könnten wir auch von „sensiblen Daten“ sprechen. Die Weitergabe persönlicher Daten, Kreditkartennummern und anderer sensibler Informationen eines Kunden kann irreparablen Schaden anrichten. Schauen Sie sich zunächst das Design Ihrer Anwendung genau an und stellen Sie fest, ob Sie diese oder jene Daten wirklich benötigen. Vielleicht benötigen Sie einige der Daten, die Sie haben, nicht wirklich – Daten, die für eine Zukunft hinzugefügt wurden, die noch nicht gekommen ist und wahrscheinlich auch nicht kommen wird. Darüber hinaus kommt es häufig vor, dass solche Daten durch die Protokollierung unbeabsichtigt verloren gehen. Eine einfache Möglichkeit, zu verhindern, dass vertrauliche Daten in Ihre Protokolle gelangen, besteht darin, die toString()- Methoden von Domänenentitäten (z. B. Benutzer, Schüler, Lehrer usw.) zu bereinigen. Dadurch verhindern Sie, dass Sie versehentlich vertrauliche Felder ausgeben. Wenn Sie Lombok verwenden, um toString() zu generieren-Methode können Sie die Annotation @ToString.Exclude verwenden , um zu verhindern, dass ein Feld in der Ausgabe der toString()- Methode verwendet wird. Seien Sie außerdem sehr vorsichtig, wenn Sie Daten an die Außenwelt senden. Angenommen, wir haben einen HTTP-Endpunkt, der die Namen aller Benutzer anzeigt. Es ist nicht erforderlich, die eindeutige interne ID eines Benutzers anzuzeigen. Warum? Denn ein Angreifer könnte damit an andere, sensiblere Informationen über den Benutzer gelangen. Wenn Sie beispielsweise Jackson zum Serialisieren/Deserialisieren eines POJO zu/von JSON verwenden , können Sie die Eigenschaften @JsonIgnore und @JsonIgnore verwendenAnmerkungen, um die Serialisierung/Deserialisierung bestimmter Felder zu verhindern. Im Allgemeinen müssen Sie an verschiedenen Orten unterschiedliche POJO-Klassen verwenden. Was bedeutet das?
  1. Wenn Sie mit einer Datenbank arbeiten, verwenden Sie einen POJO-Typ (eine Entität).
  2. Konvertieren Sie beim Arbeiten mit Geschäftslogik eine Entität in ein Modell.
  3. Wenn Sie mit der Außenwelt arbeiten und HTTP-Anfragen senden, verwenden Sie unterschiedliche Entitäten (DTOs).
So können Sie klar definieren, welche Felder von außen sichtbar sind und welche nicht.

Verwenden Sie starke Verschlüsselungs- und Hashing-Algorithmen

Vertrauliche Daten der Kunden müssen sicher gespeichert werden. Dazu müssen wir eine Verschlüsselung verwenden. Abhängig von der Aufgabenstellung müssen Sie entscheiden, welche Art der Verschlüsselung Sie verwenden möchten. Darüber hinaus nimmt eine stärkere Verschlüsselung mehr Zeit in Anspruch, sodass Sie erneut überlegen müssen, inwieweit die Notwendigkeit dafür den dafür aufgewendeten Zeitaufwand rechtfertigt. Natürlich können Sie selbst einen Verschlüsselungsalgorithmus schreiben. Aber das ist unnötig. In diesem Bereich können Sie auf bestehende Lösungen zurückgreifen. Zum Beispiel Google Tink :

<!-- https://mvnrepository.com/artifact/com.google.crypto.tink/tink -->
<dependency>
   <groupid>com.google.crypto.tink</groupid>
   <artifactid>tink</artifactid>
   <version>1.3.0</version>
</dependency>
Sehen wir uns anhand dieses Beispiels zur Ver- und Entschlüsselung an, was zu tun ist:

private static void encryptDecryptExample() {
   AeadConfig.register();
   KeysetHandle handle = KeysetHandle.generateNew(AeadKeyTemplates.AES128_CTR_HMAC_SHA256);

   String plaintext = "Elvis lives!";
   String aad = "Buddy Holly";

   Aead aead = handle.getPrimitive(Aead.class);
   byte[] encrypted = aead.encrypt(plaintext.getBytes(), aad.getBytes());
   String encryptedString = Base64.getEncoder().encodeToString(encrypted);
   System.out.println(encryptedString);

   byte[] decrypted = aead.decrypt(Base64.getDecoder().decode(encrypted), aad.getBytes());
   System.out.println(new String(decrypted));
}

Passwörter verschlüsseln

Für diese Aufgabe ist es am sichersten, asymmetrische Verschlüsselung zu verwenden. Warum? Weil die Anwendung Passwörter nicht wirklich entschlüsseln muss. Dies ist der Standardansatz. Wenn ein Benutzer in Wirklichkeit ein Passwort eingibt, verschlüsselt das System es und vergleicht es mit dem, was im Passwortspeicher vorhanden ist. Es wird der gleiche Verschlüsselungsprozess durchgeführt, sodass wir davon ausgehen können, dass sie übereinstimmen, vorausgesetzt natürlich, dass das richtige Passwort eingegeben wird :) Hier eignen sich BCrypt und SCrypt. Bei beiden handelt es sich um Einwegfunktionen (kryptografische Hashes) mit rechenintensiven Algorithmen, die viel Zeit in Anspruch nehmen. Das ist genau das, was wir brauchen, da die direkten Berechnungen ewig dauern werden (naja, eine lange, lange Zeit). Spring Security unterstützt eine ganze Reihe von Algorithmen. Wir können SCryptPasswordEncoder und BCryptPasswordEncoder verwenden. Was derzeit als starker Verschlüsselungsalgorithmus gilt, könnte im nächsten Jahr als schwach gelten. Wir kommen daher zu dem Schluss, dass wir die von uns verwendeten Algorithmen regelmäßig überprüfen und bei Bedarf die Bibliotheken, die die Verschlüsselungsalgorithmen enthalten, aktualisieren sollten.

Statt einer Schlussfolgerung

Heute haben wir über Sicherheit gesprochen und natürlich blieben viele Dinge hinter den Kulissen. Ich habe gerade die Tür zu einer neuen Welt für dich geöffnet, einer Welt, die ein Eigenleben hat. Mit Sicherheit ist es wie mit der Politik: Wenn Sie sich nicht mit Politik beschäftigen, wird sich die Politik mit Ihnen beschäftigen. Ich empfehle Ihnen traditionell, mir auf dem GitHub-Konto zu folgen . Dort poste ich meine Kreationen mit verschiedenen Technologien, die ich studiere und bei der Arbeit anwende.

Nützliche Links

  1. Guru99: SQL-Injection-Tutorial
  2. Oracle: Java Security Resource Center
  3. Oracle: Richtlinien zur sicheren Codierung für Java SE
  4. Zusammenfassung: Die Grundlagen der Java-Sicherheit
  5. Mittel: 10 Tipps zur Verbesserung Ihrer Java-Sicherheit
  6. Snyk: 10 Best Practices für die Java-Sicherheit
  7. GitHub: Ankündigung des GitHub Security Lab: Gemeinsam den Code der Welt sichern
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION