Detailing a CRIME/BREACH-like attack against JWT: the REJECTS attack

This morning I found a security hole in a cloud app I build at work. It would be completely impractical to attack given our particular setup, but I wanted to pass it along. It’s interesting in that it’s a combination of a few different innocuous things, like MacGyver making bombs out of household items.

The tl;dr here is that you’re vulnerable if:

  • If you’re using user-visible JWT, such as in cookies or HTTP headers, even if you’re encrypting them
  • And you’re using that JWT to hold secrets, such as API keys or session tokens
  • And you’re embedding user-controlled data in the JWT, such as name or email address
  • And you have JWT compression enabled

This is not a new attack. Neil Madden had a 2017 post that included a warning about this specific attack. And the class of attacks is called BREACH/CRIME and is not specific to JWT inasmuch as to any time you’re mixing secret and taintable information. The general idea goes back to at least 2002.

But because I wasn’t able to find any name for the JWT version of the attack, I’m going to go ahead and make one so that it’s easier to find with search engines. I’m calling it a REJECTS attack: Revelation of Encrypted JWT Embedded & Compressed Targeted Secrets.

Let’s break it down.

REJECTS Attack Scenario

I’m not going to explain the fundamentals of JWT, nor the finer points of JWE versus JWS. But presume that you have a JWT that includes two things: at least one field of user-controlled data and at least one secret. In practical terms, maybe this is a user name and/or email address and an API secret key.

Your unencrypted JWT payload might look something like:

    "aud": "your-app-id",
    "exp": 1573166734,
    "iat": 1573166434,
    "iss": "your-auth-provider-id",
    "nbf": 1573166429,
    "sub": "some-user-id",
    "fullName": "Alice Aardvark",
    "userName": "",
    "secret": "some-api-key-or-secret"

In that example, the built-in JWT fields are up top, the user-controlled fields are in the middle, and the system-controlled secret is at the end. Maybe that secret is a session token to some cloud service that your app relies on. Maybe it’s specific to the user, to the user’s customer account, to your app as a whole, or something else.

You think you’re safe because you’re using JWE to encrypt the JWT, and you have good access control for the encryption key. Your users see only opaque Base64-encoded blobs, right?

But you’ve enabled compression on your tokens because you know they can get kindof big. And you’ve thus made yourself vulnerable.

The attack comes because you have mixed user-controlled content (fullName and userName above) with secrets and compressed them in the same payload.

Imagine your secret is in some known format. It’s not like it’s hard to figure out the formats for secrets for popular cloud services. Most will be Base64-encoded strings of a fixed length.

For example, let’s say your secret is an API key that is just a Base64-encoded UUID: /wkW7AGxEeqNcQAANiueFQ.

An attacker can then try to brute-force discovery of that key:

  1. Change the user account’s fullName to some Base64-encoded string: AAA.
  2. Sign in and get the JWT. They can’t decrypt the JWT, but they can make note of the length.
  3. Update the user account’s fullName to a new value: AAB.
  4. Sign in and get a new JWT.
  5. Note the difference in sizes of the JWT blob. Matches between the fullName and secret will get picked up by the compression algorithm and cause the JWT to reduce in size. The better the match the smaller the size. For example, maybe eventually the attacker guesses AGx, which is in the secret. Compression would shave an extra byte or two off because of the shared sequence, and that would be noticed in the size of the JWT.
  6. Repeat.

Even if you have length restrictions on the user-controlled fields (say, 50 characters max) and the secret is significantly larger, the attacker can use windowing to get chunks of the secret at a time.

In most systems, this would be a very impractical attack. Hopefully, your app instrumentation would show the spike in user account updates, sign-in attempts, JWT regeneration, etc. Your API gateway would hopefully also pick up the activity as something suspicious. But maybe you’re running something Google/Azure/Amazon-sized and it’s harder to distinguish such activity from normal traffic.

Mitigating REJECTS Attacks

You have a bunch of options:

  • Don’t put secrets in JWT. (Okay, I know, if you’re reading this you probably don’t have much choice here.)
  • Disable JWT compression. Odds are, it’s probably not doing much for you, anyway.
  • Split your JWT into multiple JWTs, and then don’t mix JWTs that have secrets with ones that contain user-controlled data.
  • Make the secrets meaningless, by encrypting them before adding them to the JWT, with their own key.
  • Include secret-like garbage in your JWT to add false matches.

I hope that helps. I admit, I hadn’t considered such attacks before!

By Rick Osborne

I am a web geek who has been doing this sort of thing entirely too long. I rant, I muse, I whine. That is, I am not at all atypical for my breed.