
JSON Web Tokens are everywhere in modern web development, but they're also a goldmine for attackers when implemented poorly. I've seen too many developers fall into the same traps—using weak secrets, trusting unsigned tokens, or storing JWTs in localStorage like they're harmless cookies. This article breaks down the most dangerous JWT mistakes I've encountered and shows you exactly how to fix them before they bite you.
Why I'm Writing This (And Why You Should Care)
Last week, I was doing a security audit for a fintech startup. Nice team, solid product, everything looked great on the surface. Then I checked their JWT implementation. Disaster. They were accepting tokens with alg: none. Their signing secret was literally "secret123". Tokens never expired. And yes, they were storing everything in localStorage where any XSS attack could grab it all.
This isn't unique. I've seen this pattern dozens of times across different companies, different teams, different experience levels. JWT security is one of those things that seems straightforward until you realize how many ways it can go wrong.
JWT Basics: What You're Actually Working With
Let's start with the fundamentals, because understanding what JWTs actually are helps explain why they break so spectacularly.
A JWT has three parts separated by dots:
Header (algorithm and token type)
Payload (your actual data/claims)
Signature (cryptographic proof it's legit)
json{
"alg": "HS256",
"typ": "JWT"
}
The beauty of JWTs is that they're self-contained. No database lookups, no session storage, just decode and verify. But that's also the problem—if someone can forge a valid-looking token, your entire auth system falls apart.
The Greatest Hits: JWT Vulnerabilities I See Constantly
The "alg: none" Attack (My Personal Favorite)
This one blows my mind every time I see it work. Here's what happens:
Attacker grabs a valid JWT from your app
Changes the algorithm in the header to "none"
Removes the signature completely
Modifies the payload to say they're an admin
Sends it back to your server
If your code doesn't explicitly check which algorithms it accepts, it might just... accept it. No questions asked.
I've used this attack in tests more times than I can count. It worked on a major e-commerce platform just last year. CVE-2015-9235 documented this exact vulnerability across multiple JWT libraries, but somehow people keep making the same mistake.
Weak Secrets: The "password123" Problem
You know how everyone makes fun of users who use "password123"? Developers do the same thing with JWT secrets.
I maintain a list of common JWT secrets I've found during security assessments:
"secret"
"mysecret"
"jwt-secret"
"your-256-bit-secret"
Company name variations
Here's the thing about weak secrets—they're not just weak, they're publicly weak. Attackers have the same dictionaries I do. They'll your tokens in minutes, not hours.
The Immortal Token Syndrome
I once found a JWT in production with an expiration date set to the year 2099. Seriously.
Long-lived tokens are a nightmare because they give attackers extended access if compromised. And tokens will be compromised—through XSS, through logs, through careless handling. When that happens, you want the damage window to be hours, not decades.
localStorage: The Security Theater of Web Storage
This drives me crazy. Developers who would never dream of storing passwords in plain text will happily dump JWTs into localStorage.
localStorage is accessible to any script running on your domain. Any script. That means every third-party analytics tool, every ad network pixel, every piece of JavaScript has access to your authentication tokens.
I've demonstrated this attack countless times:
javascript// This is all it takes
const stolenToken = localStorage.getItem('jwt_token');
// Send to attacker's server
The "Trust But Don't Verify" Pattern
Sometimes I find code that decodes JWTs without verifying the signature. The developer figured out how to extract the claims but missed the entire point of cryptographic verification.
This turns your security token into a suggestion box. Anyone can craft whatever claims they want and your server will believe them.
Real Attacks That Actually Happened
CVE-2015-9235: When None Means None
This CVE hit multiple JWT libraries simultaneously. The vulnerability? Libraries would accept tokens with "alg": "none" and no signature as valid tokens.
The attack was elegantly simple:
Take any valid JWT
Change algorithm to "none"
Remove signature
Modify claims as desired
Libraries affected included implementations in Node.js, Python, Ruby, and others. The fix required explicitly validating that the algorithm matched what the application expected.
Real-World Bug Bounty: The Uber Case
In 2017, security researchers found JWT validation flaws in Uber's systems. They could manipulate token claims to access unauthorized endpoints and data.
This wasn't some theoretical vulnerability—it was a real attack against a production system handling millions of users and transactions.
What I've Seen in CTFs and HackTheBox
Competitive hacking platforms love JWT challenges because they're realistic. I've solved dozens of these:
Cracking weak signing secrets with dictionary attacks
Exploiting algorithm confusion vulnerabilities
Bypassing signature verification in custom implementations
Using timing attacks to determine secret length
The techniques work the same way in real applications.
Tools I Actually Use for JWT Testing]
jwt.io: Your First Stop
I always start with jwt.io when analyzing tokens. It's free, works in any browser, and shows you exactly what's in a token. But remember—attackers use this same tool to understand your tokens before exploiting them.
jwt_tool: The Professional's Choice
This Python tool by ticarpi is my go-to for serious JWT security testing. It does everything:
bash# Dictionary attack against signing secret
python3 jwt_tool.py <token> -d wordlist.txt
# Test for algorithm confusion
python3 jwt_tool.py <token> -X a
# Signature bypass attempts
python3 jwt_tool.py <token> -X s
I've used jwt_tool to find vulnerabilities in production applications. It's that effective.
jwt-hack: Quick and Testing
When I need to quickly test common JWT vulnerabilities, jwt-hack gets the job done. It's built for speed and covers the most common attack vectors.
How to Actually Fix This Stuff
Node.js: jsonwebtoken Done Right
I see this pattern constantly:
javascript// DON'T DO THIS
const jwt = require('jsonwebtoken');
const decoded = jwt.verify(token, 'secret'); // Accepts any algorithm!
Instead:
javascript// DO THIS
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
issuer: 'myapp',
audience: 'myapp-users',
maxAge: '1h'
});
Notice how I'm explicitly specifying which algorithms to accept? That prevents the alg: none attack. The issuer and audience claims help prevent token substitution attacks.
Python: PyJWT Best Practices
Similar story with Python:
pythonimport jwt
import os
# Wrong way
decoded = jwt.decode(token, 'secret', verify=False) # Never do this
# Right way
decoded = jwt.decode(
token,
os.environ['JWT_SECRET'],
algorithms=['HS256'],
issuer='myapp',
audience='myapp-users'
)
Java: Enterprise-Grade Security
Java developers, you're not off the hook:
java// Use JJWT library
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.requireIssuer("myapp")
.requireAudience("myapp-users")
.build()
.parseClaimsJws(token)
.getBody();
Go: Explicit Validation
Go makes you think about error handling, which is actually great for JWT security:
gotoken, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Validate the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected algorithm: %v", token.Header["alg"])
}
return []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil {
// Handle invalid token
return nil, err
}
My JWT Security Checklist (Learned the Hard Way)
After years of finding JWT vulnerabilities, here's what I check every time:
✅ Algorithm whitelist: Never trust the algorithm in the token header
✅ Strong secrets: Use cryptographically random secrets, rotate them regularly
✅ Claim validation: Always verify issuer, audience, and expiration
✅ Secure storage: httpOnly cookies, never localStorage
✅ Short expiration: Hours, not days or weeks
✅ Signature verification: Every token, every time, no exceptions
✅ Secret management: Environment variables or proper secret management systems
Testing Your Own Implementation
Want to know if your JWT implementation is secure? Try to break it yourself.
Here's what I do:
Generate a valid token from your app
Try the alg: none attack
Attempt to brute-force weak secrets
Test signature bypass methods
Check token storage and transmission
If any of these succeed, fix them before someone else finds them.
bash# Quick security check with jwt_tool
python3 jwt_tool.py your_token_here -X a # Algorithm confusion
python3 jwt_tool.py your_token_here -X s # Signature bypass
python3 jwt_tool.py your_token_here -d common_secrets.txt # Weak secrets
The Hard Truth About JWT Security
Here's what I've learned after finding JWT vulnerabilities in everything from startups to Fortune 500 companies:
Most JWT security problems aren't caused by complex cryptographic attacks. They're caused by developers who understand 80% of JWT security and assume the other 20% will take care of itself.
That missing 20% is where attackers live.
The alg: none attack works because developers assume their libraries handle edge cases properly. Weak secret attacks succeed because developers think "secret123" is good enough for development (and then forget to change it). Storage vulnerabilities exist because developers treat JWTs like any other piece of client-side data.
What I Wish Someone Had Told Me Earlier
When I started working with JWTs, I thought the hard part was understanding the cryptography. It's not.
The hard part is remembering that every JWT you issue is a potential attack vector. Every implementation shortcut is a security risk. Every convenience feature might be the thing that gets you compromised.
JWTs are powerful because they're flexible. They're dangerous for exactly the same reason.
Final Thoughts
I wrote this because I'm tired of seeing the same JWT vulnerabilities over and over again. These aren't exotic attacks that require advanced knowledge—they're basic mistakes that basic precautions can prevent.
Your JWT implementation probably works fine for legitimate users. The question is: what happens when someone who isn't legitimate gets their hands on your tokens?
Test it. Break it. Fix it. Before someone else does it for you.
And please, for the love of all that's secure, stop putting JWTs in localStorage.
Comments ( 0 )