Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crash when verifying a corrupt token #213

Closed
insidegui opened this issue Nov 8, 2024 · 5 comments · Fixed by #217
Closed

Crash when verifying a corrupt token #213

insidegui opened this issue Nov 8, 2024 · 5 comments · Fixed by #217
Assignees
Labels
bug Something isn't working

Comments

@insidegui
Copy link

insidegui commented Nov 8, 2024

Describe the issue

Attempting to verify a corrupt token results in the process crashing.

Vapor version

(unrelated to Vapor)

Operating system and version

macOS 14.7.1

Swift version

Swift Package Manager - Swift 6.0.3-dev

Steps to reproduce

  1. Generate a valid token
  2. Randomly change a character in the generated token header, making the base64 string invalid
  3. Attempt to verify the corrupt token using verify(token)

Below is a simple cli implementation that reproduces the issue:

EDIT: See comment for a smaller example that can reproduce the crash more reliably.

import Foundation
import JWTKit

struct TestPayload: JWTPayload {
    var sub: SubjectClaim
    var exp: ExpirationClaim
    var flag: Bool

    func verify(using algorithm: some JWTAlgorithm) async throws {
        try exp.verifyNotExpired()
    }
}

@main
struct JWTKitBugTest {
    static func main() async throws {
        do {
            let keys = JWTKeyCollection()
            await keys.add(hmac: "crashme", digestAlgorithm: .sha256)

            let test = TestPayload(sub: "hello", exp: .init(value: Date.now.addingTimeInterval(100)), flag: true)

            let token = try await keys.sign(test)

            print("Generated token: \(token)")

            _ = try await keys.verify(token, as: TestPayload.self)

            print("Verified unmodified token")

            var corruptToken = token

            /// Change a character in the token to `x`, invalidating the base64 string.
            corruptToken.replaceSubrange(token.index(token.startIndex, offsetBy: 24)..<token.index(token.startIndex, offsetBy: 25), with: "x")

            print("Corrupt token: \(corruptToken)")

            /// This crashes:
            _ = try await keys.verify(corruptToken, as: TestPayload.self, iteratingKeys: true)
        } catch {
            print(error)
        }
    }
}

Outcome

The process will crash with the fatal error below, even if the caller did not use try! and was calling within a do/catch block:

Thread 2: Fatal error: 'try!' expression unexpectedly raised an error: Foundation.JSONError.cannotConvertInputStringDataToUTF8(location: Foundation.JSONError.SourceLocation(line: 1, column: 17, index: 16))

Additional notes

The crash will not occur every time with the example above, but can be reliably reproduced by repeating the same crashing token after you've found one that does crash.

@insidegui insidegui added the bug Something isn't working label Nov 8, 2024
@insidegui insidegui changed the title Crash when verifying a corrupt token with iteratingKeys set to true Crash when verifying a corrupt token Nov 8, 2024
@insidegui
Copy link
Author

Update

Upon further investigation, I think this is actually a crash that happens in Swift's implementation of JSONDecoder when attempting to decode a String that contains an invalid byte sequence for UTF-8.

For example, the following token header has been tampered with:

eyJhbGciOiJIUzI1NiIsInR5xCI6IkpXVCJ9

Base-64 decoding that as Data, then parsing the corresponding Data using String(decoding: data, as: UTF8.self) results in this:

{"alg":"HS256","ty�":"JWT"}

I think that ty� is what's causing the crash when decoding the token.

A workaround I've found was to include a check before attempting to verify the token. In that check I validate that the token is formed of three parts and that the header and payload parts can both be decoded as UTF-8 strings by verifying that String(data: ..., encoding: .utf8) doesn't return nil. If that check doesn't pass, I abort the verification before calling verify().

@ptoffy ptoffy self-assigned this Nov 8, 2024
@0xTim
Copy link
Member

0xTim commented Nov 8, 2024

This definitely looks like a Foundation issue under the hood and should not be crashing. It's caused by the print, it works if you remove it. I'm guessing you're seeing the issue when trying to return a token to the client or similar?

@0xTim
Copy link
Member

0xTim commented Nov 8, 2024

Actually it is the verify fails, but at least for me it's not failing all the time

@insidegui
Copy link
Author

Yes, the example above will not crash 100% of the time. From what I could gather, it will only crash (in Foundation, like you've mentioned) if there's a corruption to the token in a part that ends up becoming one of the keys in the JSON string.

The way I found it was by accident while testing a server that I wrote. I wanted to check its resiliency against invalid tokens and was surprised by the crash.

It definitely looks like a bug in Swift/Foundation and not JWTKit, but I think it might be worth trying to implement some sort of workaround since this basically provides a way for someone to DoS a server by sending bad tokens.

Here's a smaller example with a hardcoded token that crashes every time:

import Foundation
import JWTKit

struct TestPayload: JWTPayload {
    var sub: SubjectClaim
    var exp: ExpirationClaim
    var flag: Bool

    func verify(using algorithm: some JWTAlgorithm) async throws {
        try exp.verifyNotExpired()
    }
}

@main
struct JWTKitBugTest {
    static func main() async throws {
        do {
            let keys = JWTKeyCollection()
            await keys.add(hmac: "crashme", digestAlgorithm: .sha256)

            let corruptToken = "eyJhbGciOiJIUzI1NiIsInR5xCI6IkpXVCJ9.eyJleHAiOjE3MzExMDkyNzkuNDIwMDM3LCJmbGFnIjp0cnVlLCJzdWIiOiJoZWxsbyJ9.iFOMv8ms0ONccGisQlzEYVe90goc3TwVD_QyztGwdCE"

            _ = try await keys.verify(corruptToken, as: TestPayload.self, iteratingKeys: true)
        } catch {
            print(error)
        }
    }
}

@ptoffy
Copy link
Member

ptoffy commented Nov 11, 2024

You're right that this does crash every time, however at this point I'm not sure what is really triggering the crash. This

eyJhbGciOiJIUzI1NiIsInR577-9IjoiSldUIn0.eyJleHAiOjE3MzExMDkyNzkuNDIwMDM3LCJzdWIiOiJoZWxsbyIsIm5hbWUiOiJCb2IiLCJhZG1pbiI6dHJ1ZX0.vvz-

is supposedly an invalid token too in that it's not UTF-8 and the header parses as {"alg":"HS256","ty�":"JWT"}, but JWTKit parses it and verifies it fine. Your base64 decoded header is {"alg":"HS256","tyÄ":"JWT"}, both are ISO-8859-1 but for some reason Foundation crashes on the latter. Safest thing is probable make a patch to check that the token is actually decodable into UTF-8 before decoding. I'll also dig deeper into the Foundation issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants