From 6f745e91e2422608fe14c9a66ee3826cb661e2a6 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni <69189821+ptoffy@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:13:06 +0100 Subject: [PATCH] Fix crash when verifying corrupted token (#217) * Fix crash when verifying corrupted token * Update UTF8 check --- Sources/JWTKit/JWTParser.swift | 18 +++++++++-- Tests/JWTKitTests/JWTKitTests.swift | 46 +++++++++++++++++++---------- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/Sources/JWTKit/JWTParser.swift b/Sources/JWTKit/JWTParser.swift index b12341c7..abc4179f 100644 --- a/Sources/JWTKit/JWTParser.swift +++ b/Sources/JWTKit/JWTParser.swift @@ -16,7 +16,8 @@ extension JWTParser { header: ArraySlice, payload: ArraySlice, signature: ArraySlice ) { let tokenParts = token.copyBytes().split( - separator: .period, omittingEmptySubsequences: false) + separator: .period, omittingEmptySubsequences: false + ) guard tokenParts.count == 3 else { throw JWTError.malformedToken(reason: "Token is not split in 3 parts") @@ -58,9 +59,20 @@ public struct DefaultJWTParser: JWTParser { let payload: Payload let signature: Data + func isUTF8(_ bytes: [UInt8]) -> Bool { + String(bytes: bytes, encoding: .utf8) != nil + } + + let headerBytes = encodedHeader.base64URLDecodedBytes() + let payloadBytes = encodedPayload.base64URLDecodedBytes() + + guard isUTF8(headerBytes) && isUTF8(payloadBytes) else { + throw JWTError.malformedToken(reason: "Header and payload must be UTF-8 encoded.") + } + do { - header = try jsonDecoder.decode(JWTHeader.self, from: .init(encodedHeader.base64URLDecodedBytes())) - payload = try jsonDecoder.decode(Payload.self, from: .init(encodedPayload.base64URLDecodedBytes())) + header = try jsonDecoder.decode(JWTHeader.self, from: .init(headerBytes)) + payload = try jsonDecoder.decode(Payload.self, from: .init(payloadBytes)) signature = Data(encodedSignature.base64URLDecodedBytes()) } catch { throw JWTError.malformedToken(reason: "Couldn't decode JWT with error: \(String(describing: error))") diff --git a/Tests/JWTKitTests/JWTKitTests.swift b/Tests/JWTKitTests/JWTKitTests.swift index 1288000b..0c3ffc78 100644 --- a/Tests/JWTKitTests/JWTKitTests.swift +++ b/Tests/JWTKitTests/JWTKitTests.swift @@ -1,7 +1,6 @@ import JWTKit import Testing import X509 -import XCTest #if !canImport(Darwin) import FoundationEssentials @@ -79,6 +78,24 @@ struct JWTKitTests { #expect(test.admin == true) } + // https://github.com/vapor/jwt-kit/issues/213 + @Test("Parse corrupt tokens") + func parseCorruptToken() throws { + let parser = DefaultJWTParser() + + // This token was created on jwt.io and is non-UTF-8 but still valid + let corruptParsableToken = + "eyJhbGciOiJIUzI1NiIsInR577-9IjoiSldUIn0.eyJleHAiOjE3MzExMDkyNzkuNDIwMDM3LCJzdWIiOiJoZWxsbyIsIm5hbWUiOiJCb2IiLCJhZG1pbiI6dHJ1ZX0.vvz-_LD_uz1K_BrxzbOWfzpOiS4hRvDztSbGiGlVujs" + _ = try parser.parse([UInt8](corruptParsableToken.utf8), as: TestPayload.self) + + // This token was created by us but has been tampered with, so it's non-UTF-8 and invalid + let corruptCrashyToken = + "eyJhbGciOiJIUzI1NiIsInR5xCI6IkpXVCJ9.eyJleHAiOjE3MzExMDkyNzkuNDIwMDM3LCJmbGFnIjp0cnVlLCJzdWIiOiJoZWxsbyJ9.iFOMv8ms0ONccGisQlzEYVe90goc3TwVD_QyztGwdCE" + #expect(throws: JWTError.malformedToken(reason: "Header and payload must be UTF-8 encoded")) { + _ = try parser.parse([UInt8](corruptCrashyToken.utf8), as: TestPayload.self) + } + } + @Test("Test Expiration") func expired() async throws { let data = @@ -491,8 +508,8 @@ struct JWTKitTests { let token = try await keyCollection.sign(payload, header: customFields) let parsed = try DefaultJWTParser().parse(token.bytes, as: TestPayload.self) - let foo = try XCTUnwrap(parsed.header.foo?.asString) - let baz = try XCTUnwrap(parsed.header.baz?.asInt) + let foo = try #require(parsed.header.foo?.asString) + let baz = try #require(parsed.header.baz?.asInt) #expect(foo == "bar") #expect(baz == 42) @@ -507,12 +524,11 @@ struct JWTKitTests { """ let jsonDecoder = JSONDecoder() - XCTAssertEqual( - try jsonDecoder.decode([String: JWTHeaderField].self, from: encodedHeader), - try jsonDecoder.decode( - [String: JWTHeaderField].self, from: jsonFields.data(using: .utf8)! - ) + let decodedFields = try jsonDecoder.decode([String: JWTHeaderField].self, from: encodedHeader) + let decodedJsonFields = try jsonDecoder.decode( + [String: JWTHeaderField].self, from: jsonFields.data(using: .utf8)! ) + #expect(decodedFields == decodedJsonFields) } @Test("Test Custom Header Fields") @@ -742,11 +758,11 @@ struct JWTKitTests { @Test("Test JWT Error Description") func jwtErrorDescription() { - XCTAssertEqual( + #expect( JWTError.claimVerificationFailure( failedClaim: ExpirationClaim(value: .init(timeIntervalSince1970: 1)), reason: "test" - ).description, - "JWTKitError(errorType: claimVerificationFailure, failedClaim: JWTKit.ExpirationClaim(value: 1970-01-01 00:00:01 +0000), reason: \"test\")" + ).description + == "JWTKitError(errorType: claimVerificationFailure, failedClaim: JWTKit.ExpirationClaim(value: 1970-01-01 00:00:01 +0000), reason: \"test\")" ) #expect( JWTError.signingAlgorithmFailure(DummyError.dummy).description @@ -789,9 +805,9 @@ struct JWTKitTests { JWTError.invalidHeaderField(reason: "test").description == "JWTKitError(errorType: invalidHeaderField, reason: \"test\")" ) - XCTAssertEqual( - JWTError.generic(identifier: "id", reason: "test").description, - "JWTKitError(errorType: generic, reason: \"test\")" + #expect( + JWTError.generic(identifier: "id", reason: "test").description + == "JWTKitError(errorType: generic, reason: \"test\")" ) } @@ -807,7 +823,7 @@ struct JWTKitTests { header.remove("field1") #expect(header.fields.count == 1) - XCTAssertNil(header.field1) + #expect(header.field1 == nil) #expect(header.field2 == .string("value2")) } }