Critical CWE-347 OWASP A02:2021

JWT Vulnerabilities in Vibe Coded Apps

How AI tools misconfigure JWT authentication, letting attackers forge tokens

Quick Answer

JWT authentication is often misconfigured by AI tools - weak secrets, missing algorithm validation, and no expiration. These flaws let attackers forge tokens and impersonate any user. Always specify the algorithm explicitly and use strong, random secrets.

#2
OWASP Ranking
CWE-347
Crypto
OWASP Category
Critical
Severity

Source: OWASP Top 10 (2021) - JWT issues fall under Cryptographic Failures

What is JWT and why does it matter?

JWT (JSON Web Token) is a compact, URL-safe way to represent claims between two parties. It's widely used for authentication in APIs and single-page applications. A JWT has three parts separated by dots: header.payload.signature. The signature proves the token hasn't been tampered with.

JWTs are ubiquitous in vibe coded applications. When you ask AI for "user authentication" or "API auth," you often get JWT implementation. The problem is AI tools generate the simplest working code - which is usually insecure.

According to Auth0's 2015 disclosure, critical vulnerabilities in JWT libraries allowed attackers to forge tokens. These same patterns still appear in AI-generated code today. CWE-347 describes improper signature verification, which is exactly what happens when AI generates JWT code without algorithm specification.

The 5 JWT flaws AI tools get wrong

These five vulnerabilities appear consistently in vibe coded JWT implementations:

1

Algorithm confusion

Using RS256 without specifying the algorithm in verify(), allowing attackers to switch to HS256 and sign with the public key.

2

None algorithm

Libraries that accept tokens with alg: "none", requiring no signature at all.

3

Weak secrets

AI generates short or predictable secrets like 'secret' or 'myapp-key' that can be brute-forced.

4

Missing expiration

Tokens without exp claim work forever. Stolen tokens remain valid indefinitely.

5

Insecure storage

Storing JWTs in localStorage makes them vulnerable to XSS attacks.

How does algorithm confusion work?

Algorithm confusion is the most dangerous JWT vulnerability. It exploits the difference between symmetric (HS256) and asymmetric (RS256) algorithms.

RS256 (Asymmetric)

  • Sign with private key
  • Verify with public key
  • Public key is... public

HS256 (Symmetric)

  • Sign with secret
  • Verify with same secret
  • Secret must stay private

The attack

  1. Server uses RS256, verifies with public key
  2. Attacker changes token header to {"alg": "HS256"}
  3. Attacker signs token with the public key (which is public!)
  4. Server calls jwt.verify(token, publicKey)
  5. Since algorithm isn't specified, library treats publicKey as HS256 secret
  6. Signature validates - attack succeeds!

According to PortSwigger, this attack works because developers don't specify which algorithm to accept during verification. The library trusts whatever algorithm is in the token header.

Why do AI tools misconfigure JWT?

AI tools generate the simplest working JWT code. "Simple" and "working" don't mean "secure."

Dangerous pattern: AI-generated JWT verification

When you ask AI for JWT authentication, it often generates this:

// VULNERABLE: AI generates this without algorithm specification
const jwt = require('jsonwebtoken')

app.get('/protected', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1]

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET) // DANGEROUS!
    req.user = decoded
    // ... protected logic
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' })
  }
})

// Without specifying algorithms, attacker can use algorithm confusion!

The code works for legitimate tokens. But without algorithms: ['HS256'], it accepts any algorithm the attacker specifies.

Dangerous pattern: Weak secrets

// VULNERABLE: Weak secrets AI tools generate
const JWT_SECRET = 'secret'              // Cracked in seconds
const JWT_SECRET = 'myapp-secret-key'    // Dictionary words
const JWT_SECRET = '12345678'            // Numeric, very weak
const JWT_SECRET = process.env.JWT_SECRET || 'fallback' // Fallback in prod!

// Attacker uses hashcat or jwt_tool to crack:
// hashcat -m 16500 jwt.txt wordlist.txt
// jwt_tool -C -d wordlist.txt jwt_token

Tools like Cursor, Bolt, and Claude Code all generate these patterns because you didn't explicitly ask for secure JWT handling.

How do I detect JWT vulnerabilities?

Search for JWT verification without algorithm specification and weak/hardcoded secrets.

Patterns to search for
// jwt.verify() without algorithms option (DANGEROUS)
jwt.verify(token, secret)
jwt.verify(token, publicKey)

// jwt.sign() without expiresIn (DANGEROUS)
jwt.sign(payload, secret)
jwt.sign({ userId }, process.env.JWT_SECRET)

// Hardcoded or weak secrets (DANGEROUS)
const JWT_SECRET = 'secret'
const secret = 'my-jwt-secret'
JWT_SECRET || 'fallback'

// localStorage JWT storage (DANGEROUS)
localStorage.setItem('token', jwt)
localStorage.getItem('token')

// Regex to find vulnerable patterns:
// jwt\.verify\s*\([^)]+\)\s*(?!.*algorithms)
// jwt\.sign\s*\([^)]+\)\s*(?!.*expiresIn)

Don't want to search manually?

Scan your code free

How do I fix JWT vulnerabilities?

Fix JWT vulnerabilities by always specifying the algorithm, using strong secrets, and setting proper expiration.

AI Fix Prompt

Copy this prompt into Cursor, Claude Code, or Bolt to automatically fix JWT issues in your codebase:

Copy-paste this prompt
Fix all JWT vulnerabilities in my codebase. ## What to look for Search for these dangerous patterns: 1. jwt.verify() without algorithms option: - jwt.verify(token, secret) - jwt.verify(token, publicKey) - Any verify() call without { algorithms: [...] } 2. jwt.sign() without expiresIn: - jwt.sign(payload, secret) - Missing expiresIn or exp claim 3. Weak or hardcoded secrets: - JWT_SECRET = 'secret' - Short secrets (< 32 chars) - process.env.JWT_SECRET || 'fallback' 4. Insecure token storage: - localStorage.setItem('token', ...) - sessionStorage (also XSS vulnerable) ## How to fix ### Fix 1: Always specify algorithm ```javascript // Before (vulnerable) const decoded = jwt.verify(token, JWT_SECRET) // After (secure) const decoded = jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'], // ONLY accept HS256 issuer: 'myapp', audience: 'myapp-users', }) // For RS256: const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'], // ONLY accept RS256 }) ``` ### Fix 2: Use strong secrets ```javascript // Generate a secure secret (run once, save to .env) const crypto = require('crypto') const secret = crypto.randomBytes(32).toString('base64') console.log(`JWT_SECRET=${secret}`) // Add to .env // In your app const JWT_SECRET = process.env.JWT_SECRET if (!JWT_SECRET || JWT_SECRET.length < 32) { throw new Error('JWT_SECRET must be at least 32 characters') } ``` ### Fix 3: Always set expiration ```javascript // Before (vulnerable - never expires) const token = jwt.sign({ userId: user.id }, JWT_SECRET) // After (secure - expires in 1 hour) const token = jwt.sign( { sub: user.id, email: user.email, role: user.role, }, JWT_SECRET, { algorithm: 'HS256', expiresIn: '1h', // Short-lived issuer: 'myapp', audience: 'myapp-api', jwtid: crypto.randomUUID(), // For revocation } ) ``` ### Fix 4: Secure token storage (frontend) ```javascript // Before (vulnerable to XSS) localStorage.setItem('token', jwtToken) // After (HttpOnly cookie - not accessible to JS) // Server-side: res.cookie('token', jwtToken, { httpOnly: true, // JavaScript can't access secure: true, // HTTPS only sameSite: 'strict', // CSRF protection maxAge: 3600000, // 1 hour }) // Client-side: fetch('/api/protected', { credentials: 'include', // Send cookies }) ``` ## Complete secure JWT implementation ```javascript const jwt = require('jsonwebtoken') const crypto = require('crypto') const JWT_SECRET = process.env.JWT_SECRET const JWT_OPTIONS = { algorithm: 'HS256', expiresIn: '1h', issuer: 'myapp', audience: 'myapp-api', } const VERIFY_OPTIONS = { algorithms: ['HS256'], issuer: 'myapp', audience: 'myapp-api', } function generateToken(user) { return jwt.sign( { sub: user.id, email: user.email, role: user.role, }, JWT_SECRET, { ...JWT_OPTIONS, jwtid: crypto.randomUUID(), } ) } function verifyToken(token) { return jwt.verify(token, JWT_SECRET, VERIFY_OPTIONS) } ``` ## After fixing 1. Search for remaining jwt.verify/jwt.sign calls 2. Verify all have algorithms specified 3. Check all secrets are strong (32+ chars) 4. Verify all tokens have expiresIn 5. Move any localStorage tokens to HttpOnly cookies 6. List all files you modified with before/after snippets Please proceed systematically through my codebase.

Manual Fix

The fix is: always specify algorithms, use strong secrets, and set expiration.

VULNERABLE
// No algorithm, weak secret, no expiration
const jwt = require('jsonwebtoken')
const JWT_SECRET = 'secret'

const token = jwt.sign({ userId: user.id }, JWT_SECRET)

const decoded = jwt.verify(token, JWT_SECRET)

// Attacker can:
// 1. Brute-force 'secret' in seconds
// 2. Use algorithm confusion if RS256
// 3. Use stolen token forever
SECURE
// Algorithm specified, strong secret, expiration set
const jwt = require('jsonwebtoken')
const JWT_SECRET = process.env.JWT_SECRET // 32+ byte random

const token = jwt.sign(
  { sub: user.id, email: user.email },
  JWT_SECRET,
  {
    algorithm: 'HS256',
    expiresIn: '1h',
    issuer: 'myapp',
  }
)

const decoded = jwt.verify(token, JWT_SECRET, {
  algorithms: ['HS256'], // Only accept HS256
  issuer: 'myapp',
})

// Now attacker cannot:
// 1. Brute-force strong secret
// 2. Use algorithm confusion (pinned to HS256)
// 3. Use expired tokens

Generate a secure secret

Run this once to generate a proper JWT secret:

Terminal
# Node.js
node -e "console.log('JWT_SECRET=' + require('crypto').randomBytes(32).toString('base64'))"

# OpenSSL
openssl rand -base64 32

# Output example:
# JWT_SECRET=K8Hy5+9cLj3xR2mN0pQwVbGfTdZaYuXkIo1hEs7C4nM=

# Add to your .env file and NEVER commit it

Frequently asked questions

What are common JWT vulnerabilities?

The most common JWT vulnerabilities are: algorithm confusion (changing RS256 to HS256), accepting the "none" algorithm, weak/guessable secrets, missing expiration claims, and insecure token storage. AI tools often generate code with weak secrets and no algorithm specification, making tokens easy to forge.

How do I secure JWT tokens?

Always specify the expected algorithm in verify() - never let the token dictate the algorithm. Use cryptographically random secrets at least 256 bits (32 bytes) long. Set reasonable expiration times with the "exp" claim. Store tokens in HttpOnly cookies, not localStorage. Verify issuer and audience claims.

What is JWT algorithm confusion attack?

Algorithm confusion happens when you use RS256 (asymmetric) but your code accepts HS256 (symmetric). Attackers change the token's algorithm to HS256 and sign with your public key. Since the server uses the same public key to verify and HS256 uses symmetric signing, the forged token validates successfully.

Should I use JWT for authentication?

JWTs are fine for authentication when implemented correctly - the problem is most implementations are not correct. If you need simple session management, server-side sessions may be safer. If you need stateless auth across services, JWTs work well but require careful implementation of algorithm pinning, strong secrets, and proper expiration.

How long should JWT secrets be?

JWT secrets should be at least 256 bits (32 bytes) of cryptographically random data. Short or predictable secrets can be brute-forced using tools like hashcat. Generate secrets using crypto.randomBytes(32).toString("base64") and store them securely in environment variables, never in code.

Related content

External resources

Scan your code for JWT vulnerabilities

Check your codebase for JWT misconfigurations and other authentication vulnerabilities in AI-generated code.

Try Vibeship Scanner