
September 09, 2026
11 min read
By Kokil Thapa | Last reviewed: September 2026
JWT security common vulnerabilities still appear in production APIs long after JSON Web Tokens became the default auth mechanism for SPAs and mobile apps. Teams treat JWTs like opaque session IDs, but they are signed documents that clients can decode, replay, and sometimes forge when validation is sloppy. If you build REST APIs with token-based authentication, you need to know where JWTs fail before an attacker finds the gap. This guide maps the real attack patterns, shows copy-paste fixes, and ties them to stacks I ship daily—Laravel with Sanctum or Passport, Symfony, and custom PHP services.
none algorithm, HMAC/RSA algorithm confusion, weak signing secrets, missing exp/aud checks, storing tokens in localStorage, and trusting client-supplied claims without server-side re-validation.What are the most common JWT security vulnerabilities?
Most JWT breaches are not exotic crypto breaks. They come from misconfiguration: the server trusts the wrong algorithm, skips claim checks, or exposes long-lived tokens where a short session would do. The RFC 7519 JWT specification defines the format, but it does not force secure defaults—that is entirely on your application code.
On client portals and booking APIs I maintain, the same mistakes recur. Developers copy a tutorial, hard-code a demo secret, and ship. Six months later, a penetration test flags token forgery or horizontal privilege escalation. Treat JWTs as credentials, not convenience strings.
The table below ranks vulnerabilities by how often I see them in code review and how severe the impact typically is. Pair this with the OWASP API Top 10 checklist for broader API hardening.
| Vulnerability | Typical cause | Impact | Fix priority |
|---|---|---|---|
Algorithm confusion (none / HS256↔RS256) | Library accepts header alg | Full account takeover | Critical |
| Weak HMAC secret | Short or default JWT_SECRET | Token forgery | Critical |
Missing exp / clock skew abuse | Validation skipped or loose | Indefinite access | High |
| Sensitive data in payload | JWT treated as encrypted | Data leak (JWT is Base64, not secret) | High |
| localStorage token storage | SPA tutorial pattern | XSS → token theft | High |
Trusting role claim alone | No DB re-check on sensitive actions | Privilege escalation | High |
| No refresh rotation / revocation | Stateless-only design | Stolen token valid until expiry | Medium |
kid header injection | Dynamic key lookup from untrusted input | Key substitution | Medium |
Compare JWT to sessions and API keys when choosing an auth model. Our JWT vs session vs API keys guide explains trade-offs that affect how many of these vulnerabilities matter for your app.
How does the "none" algorithm and key confusion attack work on JWTs?
The classic JWT forgery chain starts in the header. An attacker decodes a legitimate token—use a Base64 decoder or JSON formatter during debugging, never in production logs with live tokens—changes alg to none, strips the signature, and sends the token back. Vulnerable libraries verify nothing and accept the payload.
Algorithm confusion is subtler. A server configured for RS256 (asymmetric) may still accept HS256 (symmetric). The attacker sets alg to HS256 and signs with the server's public RSA key as the HMAC secret. If your code uses the public key bytes as the HMAC key, the signature validates. This is documented in the OWASP JWT cheat sheet.
Pin the algorithm in code, never from the header
Modern libraries like firebase/php-jwt (common in Laravel integrations) require you to pass allowed algorithms explicitly. Never pass $header['alg'] straight into verify. Example pattern for PHP 8.3+ on Laravel 12 or 13:
<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$allowedAlgs = ['RS256']; // fixed allowlist — never from token header
$decoded = JWT::decode($token, new Key($publicKeyPem, 'RS256'));
// Reject before decode if you parse manually:
if (($header['alg'] ?? '') !== 'RS256') {
throw new UnauthorizedException('Invalid algorithm');
}
Generate HMAC secrets with at least 256 bits of entropy. A cryptographically random password generator beats hand-typed strings. Store secrets in environment variables, not Git. Rotate on compromise and on staff departures with API access.
How do you prevent JWT token theft, replay, and privilege escalation?
Stolen JWTs are bearer credentials. Whoever holds the string is the user until expiry or revocation. Replay happens when an intercepted token works on another endpoint or after logout because the server keeps no deny list.
On a legal-tech client portal like Mijar Law Associates, document download and payment endpoints cannot rely on a role: admin claim alone. Re-fetch permissions from the database on sensitive routes. JWT claims identify the subject; authorization still belongs server-side.
Short lifetimes, refresh rotation, and revocation
Use short access token lifetimes—15 minutes is a common default for high-risk apps. Pair them with refresh tokens stored server-side or in rotation tables. When a user logs out, invalidate refresh tokens and add the access token jti to a Redis blocklist until natural expiry. Redis 8.10 handles this well at scale.
Combine token limits with API rate limiting so stolen tokens cannot scrape entire datasets in seconds. Log authentication failures and spikes; they often precede brute-force secret guessing.
- Issue access JWT with
exp,iat,jti, minimal claims (subonly when possible). - Store refresh tokens hashed in the database; rotate on each use.
- On password change or role change, bump a
token_versionclaim and reject older versions. - Never put PAN, passport numbers, or payment card data in JWT payloads—they are readable by anyone with the token string.
- Enable HTTPS everywhere; tokens in query strings leak via logs and Referer headers.
Should you store JWTs in localStorage or httpOnly cookies?
localStorage is convenient for SPAs but readable by any JavaScript on the page. One XSS bug—often from a third-party script or unsanitized Blade output—and the token is gone. HttpOnly, Secure, SameSite cookies keep tokens out of JavaScript reach. They introduce CSRF risk, which you mitigate with SameSite=Lax or Strict plus CSRF tokens on state-changing requests.
For Laravel apps, I often prefer Sanctum's SPA authentication cookie flow over hand-rolled JWT-in-localStorage patterns. Sanctum still uses tokens internally but manages cookie mechanics and CSRF for you. See also Content Security Policy for Laravel apps to shrink the XSS window that makes localStorage storage dangerous.
Mobile native apps cannot use httpOnly cookies the same way. Store tokens in the OS secure enclave or Keychain. Never log tokens. Avoid deep links that pass JWTs in URLs.
How do you implement secure JWT validation in Laravel and Symfony?
Laravel 13 on PHP 8.3+ gives you solid building blocks. Passport issues OAuth2 JWT access tokens; Sanctum covers SPA and simple API tokens. Both beat rolling your own crypto. I've used Sanctum and Passport on production Laravel applications for years—the failure mode is almost always custom middleware that skips checks, not the package itself.
Laravel middleware checklist
<?php
// app/Http/Middleware/VerifyJwt.php — illustrative pattern
public function handle(Request $request, Closure $next)
{
$token = $request->bearerToken();
if (!$token) {
abort(401, 'Missing token');
}
try {
$payload = JWT::decode($token, new Key(config('jwt.public_key'), 'RS256'));
} catch (\Throwable $e) {
abort(401, 'Invalid token');
}
if ($payload->exp < time()) {
abort(401, 'Token expired');
}
if ($payload->iss !== config('app.url')) {
abort(401, 'Invalid issuer');
}
$request->attributes->set('auth_user_id', $payload->sub);
return $next($request);
}
Register middleware on API route groups in bootstrap/app.php (Laravel 11+ style). Keep signing keys in storage/ or environment variables with restrictive permissions—same discipline as Ubuntu server security hardening.
For Symfony 8.1 projects, use the security firewall with a JWT authenticator from lexik/jwt-authentication-bundle. Pin algorithms in config/packages/lexik_jwt_authentication.yaml. The Symfony security firewall guide covers complementary patterns.
OAuth overlap and third-party issuers
When accepting tokens from Google, Auth0, or corporate IdPs, validate iss, aud, and fetch JWKS from a pinned HTTPS URL with cache TTL. Do not disable TLS verification. Rotate keys when the IdP publishes new kid values. Our OAuth security best practices article covers authorization-code flow mistakes that pair badly with JWT clients.
Run dependency vulnerability scanning in CI. JWT library CVEs do appear. Pin Composer and npm versions; review changelogs before upgrades on auth-critical paths.
How do you audit JWT security before production launch?
Start with a threat-model pass on auth flows: login, refresh, logout, password reset, and admin impersonation if you have it. For each flow, ask what an attacker gains with a forged, stolen, or replayed token.
- Decode sample tokens in a secure dev environment—confirm no PII or secrets in payloads.
- Attempt
alg:noneand algorithm-switch payloads against staging; all must fail with 401. - Confirm expired tokens reject even if the signature is valid.
- Verify horizontal access: user A's token must not fetch user B's records by changing
sub. - Check CORS: do not reflect arbitrary
OriginwithAccess-Control-Allow-Credentials: true. - Review logs: ensure access tokens never appear in application or CDN logs.
Dynamic testing with tools like OWASP ZAP complements manual review. For ongoing assurance, budget for security testing and optimization on apps that handle payments or confidential documents—common on Notary Nepal-class portals.
If you inherit a legacy PHP API, incremental hardening beats a rewrite. Tighten algorithm allowlists, add expiry enforcement, and migrate storage from localStorage to cookies in the next frontend release. That matches how I approach legacy systems on support and maintenance contracts.
Key Takeaways
- Pin allowed algorithms in server code; reject
noneand never derive the verify algorithm from the JWT header. - Use 256-bit+ secrets for HMAC, RS256/ES256 for multi-service setups, and short access token lifetimes with refresh rotation.
- Validate
exp,nbf,iss, andaudon every request; treat JWT claims as hints, not authorization. - Prefer httpOnly Secure SameSite cookies over localStorage for browser apps; pair with CSP and CSRF protection.
- Never store sensitive personal or payment data in JWT payloads—they are Base64-encoded, not encrypted.
- Scan JWT libraries in CI, log auth anomalies, and test forgery scenarios on staging before launch.
People Also Ask
Are JWTs encrypted?
No. Standard JWTs are signed (JWS) or optionally encrypted (JWE), but most APIs use signed tokens only. Anyone with the token string can Base64-decode the payload and read claims. Do not put confidential data inside unless you implement JWE with proper key management.
Can JWTs be revoked?
Pure stateless JWTs cannot be revoked until expiry unless you add server-side state—a blocklist keyed by jti, a session store, or a token_version field on the user record. Plan revocation before you issue long-lived tokens.
Is JWT better than session cookies for APIs?
JWTs suit distributed microservices and mobile clients where central session lookup is costly. Browser-first Laravel apps often do better with session or Sanctum cookie auth. The choice affects storage, revocation, and which JWT security common vulnerabilities apply—see our comparison article linked above.
What is the most dangerous JWT misconfiguration?
Accepting attacker-controlled signing parameters—especially alg: none or HS256 signed with a public key—ranks highest because it bypasses authentication entirely. Weak shared secrets are a close second on small teams that reuse demo keys across environments.
Ship JWT auth you can defend in a security review
JWT security common vulnerabilities are well understood and mostly preventable with strict validation, sane storage, and defence in depth. The expensive failures happen when teams treat tokens as magic strings instead of signed, time-bound credentials that still need server-side authorization. Audit your auth stack against the checklist here, patch your libraries, and test forgery paths on staging before your next release.
Need help hardening a Laravel API, legal-tech portal, or eCommerce checkout flow? Custom software development and contact us to walk through your JWT implementation—I'll flag the gaps that scanners miss and attackers don't.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

