Sicherheitskrippen: JWT



Viele Anwendungen verwenden JSON Web Tokens (JWT), damit sich der Client nach der Authentifizierung für den weiteren Informationsaustausch identifizieren kann.

JSON Web Token ist ein offener Standard (RFC 7519), der eine kompakte und eigenständige Methode zum sicheren Übertragen von Informationen zwischen Parteien als JSON-Objekt definiert.


Diese Informationen sind verifiziert und zuverlässig, da sie digital signiert sind.
JWTs können mit einem geheimen (unter Verwendung des HMAC-Algorithmus) oder öffentlichen / privaten Schlüsselpaaren unter Verwendung von RSA oder ECDSA signiert werden.

JSON Web Token wird verwendet, um Informationen über die Identität und Eigenschaften des Clients zu übertragen. Dieser "Container" wird vom Server signiert, damit der Client ihn nicht stört und beispielsweise keine Identifikationsdaten oder Merkmale ändern kann (z. B. die Rolle eines einfachen Benutzers zu einem Administrator oder die Anmeldung des Clients).

Dieses Token wird bei erfolgreicher Authentifizierung erstellt und vom Server vor dem Starten jeder Clientanforderung überprüft. Das Token wird von der Anwendung als „Ausweis“ des Kunden verwendet (ein Container mit allen Informationen über ihn). Der Server kann die Gültigkeit und Integrität des Tokens auf sichere Weise überprüfen. Auf diese Weise kann die Anwendung zustandslos sein (eine zustandslose Anwendung speichert keine in einer Sitzung generierten Clientdaten zur Verwendung in der nächsten Sitzung mit diesem Client (jede Sitzung ist unabhängig)), und der Authentifizierungsprozess ist unabhängig von den verwendeten Diensten (in dem Sinne, dass Client- und Servertechnologien) kann variieren, einschließlich des Transportkanals, obwohl HTTP am häufigsten verwendet wird).

Überlegungen zur Verwendung von JWT


Selbst wenn das JWT-Token einfach zu verwenden ist und es Ihnen ermöglicht, Dienste (hauptsächlich REST) ​​ohne Status (zustandslos) bereitzustellen, ist diese Lösung nicht für alle Anwendungen geeignet, da sie einige Einschränkungen aufweist, z. B. das Problem des Speicherns des Tokens.

Wenn die Anwendung nicht vollständig zustandslos sein muss, können Sie das herkömmliche Sitzungssystem verwenden, das von allen Webplattformen bereitgestellt wird. Für zustandslose Anwendungen ist JWT jedoch eine gute Option, wenn es korrekt implementiert ist.

JWT-Probleme und Angriffe


Verwenden des NONE-Hash-Algorithmus


Ein ähnlicher Angriff tritt auf, wenn ein Angreifer das Token und auch den Hashing-Algorithmus (Feld "alg") ändert, um durch das Schlüsselwort none anzuzeigen, dass die Token-Integrität bereits überprüft wurde. Einige Bibliotheken betrachteten Token, die mit dem Algorithmus none signiert wurden, als gültiges Token mit einer verifizierten Signatur, sodass ein Angreifer die Nutzlast des Tokens ändern konnte und die Anwendung dem Token vertrauen würde.

Um einen Angriff zu verhindern, müssen Sie die JWT-Bibliothek verwenden, die von dieser Sicherheitsanfälligkeit nicht betroffen ist. Während der Validierung des Tokens müssen Sie außerdem explizit die Verwendung des erwarteten Algorithmus anfordern.

Implementierungsbeispiel:

//  HMAC   String   JVM private transient byte[] keyHMAC = ...; ... //        //    HMAC-256 - JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC)).build(); //   DecodedJWT decodedToken = verifier.verify(token); 

Token-Abfangen


Der Angriff tritt auf, wenn ein Token von einem Angreifer abgefangen oder gestohlen wurde und er verwendet, um mithilfe der Anmeldeinformationen eines bestimmten Benutzers Zugriff auf das System zu erhalten.

Der Schutz besteht darin, dem Token einen „Benutzerkontext“ hinzuzufügen. Der Benutzerkontext besteht aus folgenden Informationen:

  1. Eine zufällige Zeichenfolge, die in der Authentifizierungsphase generiert und im Token enthalten ist und auch als sichereres Cookie an den Client gesendet wird (Flags: HttpOnly + Secure + SameSite + Cookie-Präfixe).
  2. Der SHA256-Hash aus der zufälligen Zeichenfolge wird im Token gespeichert, sodass der Angreifer bei einem XSS-Problem den Wert der zufälligen Zeichenfolge nicht lesen und das erwartete Cookie setzen kann.

Die IP-Adresse wird im Kontext nicht verwendet, da es Situationen gibt, in denen sich die IP-Adresse während einer Sitzung ändern kann, z. B. wenn ein Benutzer über sein Mobiltelefon auf die Anwendung zugreift. Dann ändert sich die IP-Adresse ständig rechtmäßig. Darüber hinaus kann die Verwendung einer IP-Adresse möglicherweise Probleme bei der Einhaltung der europäischen DSGVO verursachen.

Wenn das empfangene Token während der Tokenüberprüfung nicht den richtigen Kontext enthält, muss es abgelehnt werden.
Implementierungsbeispiel:

Code zum Erstellen eines Tokens nach erfolgreicher Authentifizierung:

 //  HMAC   String   JVM private transient byte[] keyHMAC = ...; //    private SecureRandom secureRandom = new SecureRandom(); ... //   ,     byte[] randomFgp = new byte[50]; secureRandom.nextBytes(randomFgp); String userFingerprint = DatatypeConverter.printHexBinary(randomFgp); //    cookie String fingerprintCookie = "__Secure-Fgp=" + userFingerprint + "; SameSite=Strict; HttpOnly; Secure"; response.addHeader("Set-Cookie", fingerprintCookie); // SHA256          // (  )  XSS      //     cookie MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] userFingerprintDigest = digest.digest(userFingerprint.getBytes("utf-8")); String userFingerprintHash = DatatypeConverter.printHexBinary(userFingerprintDigest); //      15     Calendar c = Calendar.getInstance(); Date now = c.getTime(); c.add(Calendar.MINUTE, 15); Date expirationDate = c.getTime(); Map<String, Object> headerClaims = new HashMap<>(); headerClaims.put("typ", "JWT"); String token = JWT.create().withSubject(login) .withExpiresAt(expirationDate) .withIssuer(this.issuerID) .withIssuedAt(now) .withNotBefore(now) .withClaim("userFingerprint", userFingerprintHash) .withHeader(headerClaims) .sign(Algorithm.HMAC256(this.keyHMAC)); 


Code zur Überprüfung der Gültigkeit des Tokens:
 //  HMAC   String   JVM private transient byte[] keyHMAC = ...; ... //     cookie String userFingerprint = null; if (request.getCookies() != null && request.getCookies().length > 0) { List<Cookie> cookies = Arrays.stream(request.getCookies()).collect(Collectors.toList()); Optional<Cookie> cookie = cookies.stream().filter(c -> "__Secure-Fgp" .equals(c.getName())).findFirst(); if (cookie.isPresent()) { userFingerprint = cookie.get().getValue(); } } //  SHA256      cookie  //       MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] userFingerprintDigest = digest.digest(userFingerprint.getBytes("utf-8")); String userFingerprintHash = DatatypeConverter.printHexBinary(userFingerprintDigest); //      JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC)) .withIssuer(issuerID) .withClaim("userFingerprint", userFingerprintHash) .build(); //   DecodedJWT decodedToken = verifier.verify(token); 

Expliziter Widerruf des Tokens durch den Benutzer


Da das Token erst nach Ablauf ungültig wird, verfügt der Benutzer nicht über eine integrierte Funktion, mit der Sie das Token explizit abbrechen können. Im Falle eines Diebstahls kann der Benutzer den Token daher nicht selbst zurückziehen und den Angreifer dann blockieren.

Eine der Schutzmethoden ist die Einführung einer schwarzen Liste von Token, die zur Simulation der in einem herkömmlichen Sitzungssystem vorhandenen "Abmelde" -Funktion geeignet ist.

Die Sammlung (in SHA-256-Codierung in HEX) des Tokens mit dem Stornierungsdatum, das die Gültigkeitsdauer des ausgestellten Tokens überschreiten sollte, wird in der schwarzen Liste gespeichert.

Wenn der Benutzer sich "abmelden" möchte, ruft er einen speziellen Dienst auf, der das bereitgestellte Benutzertoken zur schwarzen Liste hinzufügt, was zur sofortigen Löschung des Tokens zur weiteren Verwendung in der Anwendung führt.

Implementierungsbeispiel:

Blacklist-Repository:
Für die zentrale Speicherung der schwarzen Liste wird eine Datenbank mit folgender Struktur verwendet:

 create table if not exists revoked_token(jwt_token_digest varchar(255) primary key, revokation_date timestamp default now()); 

Token-Widerrufsverwaltung:

 //    (logout). //  ,      //         . public class TokenRevoker { //    @Resource("jdbc/storeDS") private DataSource storeDS; //      public boolean isTokenRevoked(String jwtInHex) throws Exception { boolean tokenIsPresent = false; if (jwtInHex != null && !jwtInHex.trim().isEmpty()) { //   byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex); //  SHA256   MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] cipheredTokenDigest = digest.digest(cipheredToken); String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest); //     try (Connection con = this.storeDS.getConnection()) { String query = "select jwt_token_digest from revoked_token where jwt_token_digest = ?"; try (PreparedStatement pStatement = con.prepareStatement(query)) { pStatement.setString(1, jwtTokenDigestInHex); try (ResultSet rSet = pStatement.executeQuery()) { tokenIsPresent = rSet.next(); } } } } return tokenIsPresent; } //    HEX      public void revokeToken(String jwtInHex) throws Exception { if (jwtInHex != null && !jwtInHex.trim().isEmpty()) { //   byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex); //  SHA256   MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] cipheredTokenDigest = digest.digest(cipheredToken); String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest); //             //   if (!this.isTokenRevoked(jwtInHex)) { try (Connection con = this.storeDS.getConnection()) { String query = "insert into revoked_token(jwt_token_digest) values(?)"; int insertedRecordCount; try (PreparedStatement pStatement = con.prepareStatement(query)) { pStatement.setString(1, jwtTokenDigestInHex); insertedRecordCount = pStatement.executeUpdate(); } if (insertedRecordCount != 1) { throw new IllegalStateException("Number of inserted record is invalid," + " 1 expected but is " + insertedRecordCount); } } } } } 

Token-Offenlegung


Dieser Angriff tritt auf, wenn ein Angreifer Zugriff auf ein Token (oder eine Reihe von Token) erhält und die darin gespeicherten Informationen extrahiert (Informationen zum JWT-Token werden mit base64 codiert), um Informationen über das System zu erhalten. Informationen können beispielsweise Sicherheitsrollen, Anmeldeformat usw. sein.

Die Schutzmethode liegt auf der Hand und besteht in der Verschlüsselung des Tokens. Es ist auch wichtig, verschlüsselte Daten mithilfe der Kryptoanalyse vor Angriffen zu schützen. Um all diese Ziele zu erreichen, wird der AES-GCM-Algorithmus verwendet, der die authentifizierte Verschlüsselung mit zugehörigen Daten (AEAD) ermöglicht. Das AEAD-Grundelement bietet symmetrische authentifizierte Verschlüsselungsfunktionen. Implementierungen dieses Grundelements sind vor adaptiven Angriffen basierend auf ausgewähltem Chiffretext geschützt. Bei der Verschlüsselung von Klartext können Sie optional verwandte Daten angeben, die authentifiziert, aber nicht verschlüsselt werden müssen.

Das heißt, die Verschlüsselung mit den relevanten Daten stellt die Authentizität und Integrität der Daten sicher, nicht jedoch deren Geheimhaltung.

Es ist jedoch zu beachten, dass die Verschlüsselung hauptsächlich hinzugefügt wird, um interne Informationen zu verbergen. Es ist jedoch sehr wichtig, sich daran zu erinnern, dass der anfängliche Schutz vor Fälschungen des JWT-Tokens die Signatur ist. Daher sollte immer die Signatur des Tokens und seine Überprüfung verwendet werden.

Clientseitiger Token-Speicher


Wenn die Anwendung das Token so speichert, dass eine oder mehrere der folgenden Situationen auftreten:

  • Das Token wird automatisch vom Browser gesendet (Cookie-Speicher).
  • Das Token wird auch dann erhalten, wenn der Browser neu gestartet wird (mithilfe des Browsers localStorage container).
  • Das Token wird im Falle eines XSS-Angriffs abgerufen (Cookie für JavaScript-Code verfügbar oder ein Token, das in localStorage oder sessionStorage gespeichert ist).

So verhindern Sie einen Angriff:

  1. Speichern Sie das Token im Browser mithilfe des sessionStorage-Containers.
  2. Fügen Sie es mithilfe des Bearer-Schemas zum Authorization-Header hinzu. Der Titel sollte folgendermaßen aussehen:

     Authorization: Bearer <token> 
  3. Fügen Sie dem Token Fingerabdruckinformationen hinzu.

Durch Speichern des Tokens im sessionStorage-Container wird im Fall von XSS ein Token für Diebstahl bereitgestellt. Ein dem Token hinzugefügter Fingerabdruck verhindert jedoch, dass ein Angreifer das gestohlene Token auf seinem Computer wiederverwendet. Fügen Sie eine Inhaltssicherheitsrichtlinie hinzu, um den Ausführungskontext einzuschränken, um die maximalen Nutzungsbereiche für einen Angreifer zu schließen.

Es bleibt ein Fall, in dem ein Angreifer den Browserkontext des Benutzers als Proxyserver verwendet, um die Zielanwendung über einen legitimen Benutzer zu verwenden. Die Inhaltssicherheitsrichtlinie kann jedoch die Kommunikation mit unerwarteten Domänen verhindern.

Es ist auch möglich, einen Authentifizierungsdienst zu implementieren, sodass das Token in einem sicheren Cookie ausgegeben wird. In diesem Fall sollte jedoch ein Schutz gegen CSRF implementiert werden.

Verwenden eines schwachen Schlüssels zum Erstellen eines Tokens


Wenn das im Fall des HMAC-SHA256-Algorithmus verwendete Geheimnis, das zum Signieren des Tokens erforderlich ist, schwach ist, kann es gehackt werden (mithilfe eines Brute-Force-Angriffs aufgegriffen werden). Infolgedessen kann ein Angreifer ein beliebig gültiges Token als Signatur vortäuschen.

Um dieses Problem zu vermeiden, müssen Sie einen komplexen geheimen Schlüssel verwenden: alphanumerisch (gemischte Groß- und Kleinschreibung) + Sonderzeichen.

Da der Schlüssel nur für Computerberechnungen benötigt wird, kann die Größe des geheimen Schlüssels 50 Positionen überschreiten.

Zum Beispiel:

 A&'/}Z57M(2hNg=;LE?~]YtRMS5(yZ<vcZTA3N-($>2j:ZeX-BGftaVk`)jKP~q?,jk)EMbgt*kW' 

Um die Komplexität des für Ihre Tokensignatur verwendeten geheimen Schlüssels zu beurteilen, können Sie in Kombination mit der JWT-API einen Kennwortwörterbuchangriff auf das Token anwenden.

Source: https://habr.com/ru/post/de457090/


All Articles