Your JWT Payload Is Not Encrypted. A 2015 Bug Let Attackers Change It by Setting the Algorithm to 'None'.

ToolHQ TeamSeptember 15, 20266 min read

There is a string sitting in a browser's localStorage right now on millions of websites that looks like gibberish but contains your user ID, your email address, your account role, and an expiration time. Anyone who opens the browser developer tools and types localStorage.getItem('token') can read it in plain text. The information is not encrypted. It is Base64-encoded, which is not encryption, and it is signed, which means a server can verify it has not been tampered with.

That string is a JSON Web Token. The design is intentional and the confusion about it is nearly universal.

What JWT Was Designed to Solve

Before JWT became standard, the common approach to web authentication used server-side sessions. A user logs in, the server creates a session record in a database keyed to a session ID, and the session ID is stored in a cookie. On every subsequent request, the server looks up the session ID in the database to verify the user. This works but creates a state-management problem: every server in a cluster needs access to the session database, which becomes a centralized dependency.

JSON Web Tokens emerged from the OAuth 2.0 ecosystem around 2010 as a way to pass user identity information between systems without requiring shared session storage. The specification, RFC 7519, was published by the IETF in May 2015. Michael Jones, John Bradley, and Nat Sakimura authored the RFC. JWT became part of the JOSE (JSON Object Signing and Encryption) framework, which also defines JWS (JSON Web Signature) and JWE (JSON Web Encryption) standards.

The key design insight was statelessness: if the token itself contains all the information the server needs, the server does not need to query a database to verify the request. The server only needs to verify the signature. This scales horizontally; any server instance can verify any token as long as it has the signing key.

How a JWT Is Structured

A JWT consists of three parts separated by periods: the header, the payload, and the signature. Each part is Base64url-encoded. Base64url is a variant of Base64 that replaces the + and / characters with - and _ to make the string safe to use in URLs and HTTP headers.

The header identifies the token type (JWT) and the signing algorithm (commonly RS256 or HS256). The payload contains claims: statements about the user and additional context. Standard claims include sub (subject, typically a user ID), iss (issuer), aud (audience), iat (issued at, a Unix timestamp), and exp (expiration, a Unix timestamp). Applications add custom claims for application-specific data like user role, tenant ID, or permission flags.

A decoded payload might contain a user ID, an email address, a role field, an issued-at timestamp, and an expiration timestamp. All of this is readable to anyone who has the token. The signature does not conceal the payload; it proves that the payload was produced by a server with the signing key and has not been altered since.

The security model is: the server trusts the token because it can verify the signature. If the token is valid and not expired, the user is authenticated without a database query. If the token is expired, the user must re-authenticate or present a refresh token.

The Algorithm Confusion Vulnerabilities

The algorithm field in the JWT header is part of what the server reads to verify the signature. In 2015, security researcher Tim McLean documented a vulnerability in multiple JWT libraries: if the algorithm field was set to "none," many libraries would skip signature verification entirely and accept any payload as valid. An attacker who had intercepted a valid token could change the algorithm to "none," modify the payload to claim administrator access or a different user ID, remove the signature, and the server would accept the modified token as valid.

This class of vulnerability, where a security check is controlled by the data being verified, is called a confused deputy problem. The attacker was using the server as a confused deputy that accepted the attacker's instructions about how to check the attacker's own credentials. The vulnerability was widespread enough to affect multiple widely-used libraries. The JWT specification was clarified, and library implementations added explicit rejection of the "none" algorithm for servers that require signature verification.

A second common vulnerability involves algorithm confusion between asymmetric and symmetric signatures. RS256 uses an RSA asymmetric key pair: the server signs with a private key and verifies with a public key that can be shared openly. HS256 uses a single symmetric key for both signing and verification, which must be kept secret. Some JWT libraries, when presented with a token claiming to use HS256 but where the server expected RS256, would use the server's public RSA key as the HMAC secret. An attacker who knew the public key, which is often published by the server for external verifiers, could craft a valid HS256-signed token, and the library would verify it using the public key as the HMAC secret. The result was the same: forged tokens accepted as valid.

These vulnerabilities share a structural root: the token itself told the server how to verify the token. The fix in both cases was to make the server specify the expected algorithm rather than reading it from the token.

JWT Storage and the XSS Trade-off

Storing JWTs in browser localStorage, as many tutorials demonstrate, creates an XSS (cross-site scripting) vulnerability. Any JavaScript running on the page can read localStorage, including malicious scripts injected by XSS attacks. A compromised script can read the token and exfiltrate it, giving an attacker access to the account without the user's credentials.

The alternative is storing JWT tokens in HttpOnly cookies. HttpOnly cookies cannot be read by JavaScript; the browser includes them in requests automatically but no script can access their contents. This eliminates the XSS token theft vector but introduces a CSRF (cross-site request forgery) risk, which requires additional mitigation like SameSite cookie attributes or CSRF tokens.

Security practitioners are divided on JWT versus server-side sessions for web applications. Critics of JWT point to its complexity, the history of implementation vulnerabilities, and the difficulty of invalidating tokens before they expire. A user whose account is compromised cannot have their JWT revoked without either maintaining a server-side token blacklist, which reintroduces state, or waiting for the token to expire naturally. Proponents point to the horizontal scalability advantage and its suitability for distributed systems where service-to-service calls need portable identity.

Why Reading a Token Is a Basic Debugging Skill

Even setting aside security concerns, decoding a JWT is a fundamental debugging task. When authentication fails, the first question is: what does the token actually contain? Is the user ID correct? Is the role field populated? Is the token expired? Was it issued by the expected issuer?

Conclusion

The signature cannot be verified without the server's secret key. But decoding the header and payload to read their contents requires no key, because they are not encrypted. A JWT decoder simply reverses the Base64url encoding and formats the JSON for readability.

ToolHQ's JWT decoder shows the header, payload, and signature components of any token you paste. The header reveals which algorithm was used to sign the token. The payload reveals all claims the server embedded. The expiration timestamp tells you whether the token is still valid.

Frequently Asked Questions

Is the content of a JWT token encrypted?

No. JWT payloads are Base64url-encoded, not encrypted. Anyone with access to the token can decode and read the payload. JWTs are signed to prevent tampering, not encrypted to prevent reading.

What was the JWT 'none' algorithm vulnerability?

In 2015, researcher Tim McLean found that many JWT libraries would skip signature verification if the header's algorithm field was set to 'none,' allowing attackers to forge tokens with any payload.

Can I verify a JWT signature without the server's secret?

For HMAC-signed tokens (HS256), no. For RSA-signed tokens (RS256), yes if you have the public key, which servers often publish. Decoding the payload requires no key at all.

What information is typically stored in a JWT payload?

Common claims include sub (user ID), email, role, iat (issued at timestamp), and exp (expiration timestamp). Custom applications add whatever fields their authorization logic requires.

Try These Free Tools