Reading a JWT Offline: How to Spot alg:none and Algorithm Confusion Before They Bite

Written by

in

A pentester friend sent me a JWT last month with a one-line note: “spot the bug in 10 seconds.” I pasted the three segments into Base64Lab, flipped on URL-safe decoding, and read the header. The alg field said none. That token had no signature at all, and the backend was accepting it. Ten seconds, exactly.

Most JWT bugs aren’t cryptographic. They’re the kind you catch by just reading the token — if you can decode it without shipping it to a random website first. Here’s how I read tokens offline and the three things I look for every time.

A JWT is three Base64url blobs, not encryption

People treat JWTs like ciphertext. They’re not. A JWT is three chunks joined by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsInJvbGUiOiJ1c2VyIn0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
[----- header -----].[--------- payload ---------].[------------- signature -------------]

The header and payload are plain Base64url-encoded JSON. Anyone holding the token can read them. The signature is the only part that’s cryptographic, and it only proves the header and payload haven’t been tampered with — it does not hide anything.

The catch: JWTs use Base64url, not standard Base64. RFC 7515 swaps + for -, / for _, and strips the trailing = padding so tokens survive inside URLs. Paste a raw JWT segment into a standard Base64 decoder and it often chokes on the missing padding or the -_ characters. That’s why I keep the URL-safe toggle on in Base64Lab — it undoes the substitution and re-adds padding before decoding, so each segment comes out as clean JSON.

Bug #1: alg is “none”

The original JWT spec allowed an alg value of none, meaning “this token is unsigned, trust it anyway.” It was meant for cases where transport security already handled integrity. In practice it became one of the most reliable auth bypasses on the web.

The attack: take a valid token, change the payload to "role":"admin", set the header to {"alg":"none"}, and drop the signature entirely. Libraries that honored none would accept it. CVE-2015-9235 (jsonwebtoken), CVE-2016-5431, and a long tail of copycats all trace back to this.

So the first thing I decode is the header. Grab the part before the first dot and decode it:

// header segment
eyJhbGciOiJub25lIn0
// decoded
{"alg":"none"}

If you ever see none in production, that’s a critical finding. Your validation library should reject it outright — modern versions of most libraries do, but only if you pin the expected algorithm on the verify call.

Bug #2: HS256 where you expected RS256

This one is subtler and still bites people in 2026. RS256 signs with a private key and verifies with a public key. HS256 signs and verifies with the same shared secret. The algorithm-confusion attack swaps RS256 for HS256, then signs the forged token using the server’s public key as the HMAC secret — and the public key is, by definition, public.

If a verify function is written like this, it’s vulnerable:

// BAD: trusts whatever alg the token claims
jwt.verify(token, keyOrSecret);

Because the library picks the algorithm from the attacker-controlled header. The fix is to pin it:

// GOOD: server dictates the algorithm
jwt.verify(token, publicKey, { algorithms: ['RS256'] });

Reading the header offline tells you instantly which algorithm a token claims. If your service issues RS256 tokens but you’re staring at an HS256 header, someone is probing you.

Bug #3: secrets and PII sitting in the payload

The payload is not a secret. I’ve decoded production tokens and found full email addresses, internal user IDs, feature flags, and — twice — what looked like a hashed password stuffed into a custom claim. Anyone who intercepts the token, or pulls it out of a browser’s localStorage, reads all of it.

Decode the middle segment and actually look at what you’re shipping to the client:

{"sub":"12345","role":"user","email":"[email protected]","iat":1752000000,"exp":1752003600}

Check the exp claim too. It’s a Unix timestamp. If it’s missing, the token never expires, which turns a single leaked token into permanent access. (If you want to sanity-check those timestamps, our Unix timestamp converter turns 1752003600 into a human date in one paste.)

Why I decode offline, every time

The obvious way to read a JWT is to paste it into one of the popular online decoders. I stopped doing that, and I think you should too.

A JWT is a live credential. For as long as it hasn’t expired, it is the logged-in session. Pasting a production token into a third-party website means handing your auth to whatever that site’s server does with the request — logging, analytics, a compromised CDN, a curious employee. The token in the RFC 7519 examples is harmless. The one from your staging environment at 2am is not.

Base64Lab does the decode entirely in the browser. There’s no server round-trip — the JSON never leaves your machine, so a token you paste to inspect stays local. You can confirm it yourself: open the page, kill your network connection, and it still decodes. That’s the property I want from anything touching a credential. I wrote more about that reasoning in why I stopped pasting JWTs into online decoders.

My 30-second token triage

When a token lands in front of me, the routine is always the same:

  1. Split on the dots into three parts.
  2. Decode the header (URL-safe on). Check alg — reject none, question anything that doesn’t match what the service issues.
  3. Decode the payload. Scan for PII or secrets that shouldn’t be there. Confirm exp exists and is sane.
  4. Leave the signature alone — you can’t verify it without the key, and you don’t need to for triage.

None of this requires a CLI, a library, or an internet connection to a decoder that logs your input. It’s reading JSON. The only trick is a decoder that understands Base64url and keeps the data on your machine.

If you spend real time in auth code or security reviews, a physical reference beats a browser tab. I keep a copy of Web Application Security by Andrew Hoffman (O’Reilly) on the desk — its token and session chapters are the clearest treatment of this class of bug I’ve found. Full disclosure: that’s an Amazon affiliate link.

Decode your next JWT offline in Base64Lab — URL-safe toggle on, no upload, no server. Then go check whether your verify calls actually pin the algorithm.


Want signal without the noise? Join https://t.me/alphasignal822 for free market intelligence.

📧 Get weekly insights on security, trading, and tech. No spam, unsubscribe anytime.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Also by us: StartCaaS — AI Company OS · Hype2You — AI Tech Trends