Skip to content

Commit

Permalink
Fix crash when verifying corrupted token (#217)
Browse files Browse the repository at this point in the history
* Fix crash when verifying corrupted token

* Update UTF8 check
  • Loading branch information
ptoffy authored Nov 11, 2024
1 parent 087031e commit 6f745e9
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 18 deletions.
18 changes: 15 additions & 3 deletions Sources/JWTKit/JWTParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ extension JWTParser {
header: ArraySlice<UInt8>, payload: ArraySlice<UInt8>, signature: ArraySlice<UInt8>
) {
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")
Expand Down Expand Up @@ -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))")
Expand Down
46 changes: 31 additions & 15 deletions Tests/JWTKitTests/JWTKitTests.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import JWTKit
import Testing
import X509
import XCTest

#if !canImport(Darwin)
import FoundationEssentials
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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)

Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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\")"
)
}

Expand All @@ -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"))
}
}
Expand Down

0 comments on commit 6f745e9

Please sign in to comment.