JWT alg:none Bypass — Why Your Token Validation Is Broken
A 72-hour breach from one JWT header field.
- API security protects endpoints from unauthorized access, injection, replay attacks, and denial-of-service — defense in depth across auth, rate limiting, validation, and monitoring
- Key components: OAuth 2.0 + JWT (auth), rate limiting (DoS protection), input validation (injection), TLS (encryption), audit logging (forensics)
- Performance impact: JWT validation adds 2-5ms, rate limiting adds 1-3ms Redis round-trip, input validation ~1ms
- Production trap: JWT with alg:none accepted by misconfigured library — attacker bypasses signature verification entirely
- Biggest mistake: Trusting user input without validation — SQL injection, XSS, and NoSQL injection start at the API boundary
Imagine your API is a bank vault. The vault has a door (authentication), a guard who checks your ID (authorization), a camera watching for suspicious behavior (rate limiting), and a rule that says nobody can slip a fake deposit slip through the slot (input validation). API security is the complete system of locks, guards, and alarms — not just the front door. Miss any one piece and the whole vault is compromised.
Every second, APIs are being probed, fuzzed, replayed, and abused at scale. The 2023 Salt Security API Security report found that 94% of organizations experienced security problems in production APIs. The OWASP API Security Top 10 reads like a greatest-hits album of real breaches — from the Peloton user data leak (broken object-level authorization) to the Twitter 5.4M account scrape (broken function-level authorization). APIs are the attack surface that never sleeps.
The core problem is that APIs are designed for machine-to-machine communication, which means they're verbose, consistent, and predictable — all properties that attackers love. A human logging into a web app triggers a CAPTCHA, gets rate-limited by IP, and shows up in session logs. An automated script hammering your REST API at 10,000 requests per second looks identical to a legitimate integration partner unless you've built defense in depth from day one.
By the end you'll understand not just the what but the why behind every major API security control. You'll be able to threat-model an API surface, implement JWT validation correctly (including the alg:none exploit), design a rate limiter that survives distributed attackers, and build input validation that stops injection attacks before they reach your database. This is the article you bring to your next architecture review.
Authentication and Authorization — The Two Doors
Authentication (AuthN) verifies who you are — your identity. Authorization (AuthZ) verifies what you're allowed to do — your permissions. They are not the same thing, and confusing them leads to broken access control, the #1 OWASP API risk.
JWT (JSON Web Tokens) are the most common AuthN mechanism for APIs. A JWT contains a header (algorithm), payload (claims like sub, exp, roles), and signature. The signature prevents tampering — but only if you validate it correctly.
The critical JWT validation steps are: verify the signature using the correct algorithm and key; check the expiration (exp) claim is in the future; check the not-before (nbf) claim is in the past; validate the issuer (iss) if your API only trusts one issuer; and crucially, reject the 'none' algorithm. Many JWT libraries accept 'alg':'none' for backward compatibility. An attacker can change the algorithm to 'none', remove the signature, and the library will accept the token as valid.
OAuth 2.0 is the framework for authorization delegation — letting a third-party access your API on behalf of a user without seeing their password. It uses grants: authorization code (most common, for web apps), client credentials (machine-to-machine), and refresh tokens (long-lived access renewal). The access token (often a JWT) is short-lived; the refresh token is long-lived and stored securely by the client.
JWT.require(Algorithm.HMAC256(secret)) NOT JWT.require(algorithm) with algorithm from the token header./.well-known/jwks.json.Rate Limiting — Protecting APIs from Abuse and DoS
Rate limiting is the shield between your API and a DDoS attack or a misconfigured client. Without it, a single user or botnet can consume all your database connections, saturate your CPU, or rack up cloud costs.
The core rate limiting strategies are: Fixed window (count requests per minute, reset on the minute) — simple but bursty; Sliding window (count requests in the last 60 seconds) — smoother but more memory-intensive; Token bucket (refill tokens at a fixed rate) — allows bursts up to bucket size; and Leaky bucket — queues requests and processes at a fixed rate.
For distributed systems, rate limit state must be shared across gateway instances. Redis with atomic INCR + EXPIRE is the standard solution. Key format: rate_limit:{userId}:{window_timestamp}. Each request increments the key; if the count exceeds limit, reject.
Attackers bypass naive rate limiters by: spreading requests across multiple IPs (use user ID or API key as the key); spreading across multiple endpoints (use aggregate rate limit per user across all endpoints); or using headers like X-Forwarded-For (validate and normalize IP headers, never trust them directly).
tokens and last_refill_timestamp. Burst size = bucket capacity.Input Validation — Stopping Injection at the Door
Input validation is the single most cost-effective security control. It's the fence at the top of the cliff, not the ambulance at the bottom. Validate everything, reject anything unexpected, and do it before authentication to save resources.
The types of injection attacks are: SQL injection — attacker sends ' OR '1'='1 to bypass login or extract data; NoSQL injection — operators like $ne and $where injected into MongoDB queries; Command injection — semicolons and pipes to execute arbitrary system commands; and XSS (Cross-Site Scripting) — script tags in user input that execute in browsers.
Validate for type, length, format, and range. A UUID field should reject non-UUID strings early. An email field should reject strings over 254 characters. A status field with enum values should reject arbitrary strings. Use allowlisting (allow known-good patterns) not denylisting (block known-bad patterns) — attackers always find new bad patterns.
Sanitization (escaping) and parametrization are different from validation. Parameterized SQL queries separate code from data, making injection structurally impossible. Input validation rejects malicious input before it reaches the database, but parameterization is the primary defense for SQL injection — validation alone is not sufficient.
maxDepth and maxStringLength constraints.The JWT alg:none Exploit That Bypassed Authentication
{"alg":"none"} in the header and an empty signature. The library saw alg:none, skipped signature verification entirely, and considered the token valid. The attacker could set any payload — including {"sub":"admin","role":"administrator"} — and the API would accept it. The breach continued for 72 hours until the anomaly in token size alerted the team.JWT.require(Algorithm.HMAC256(secret)).acceptLeeway(60).build().verify(token); for symmetric keys, or Algorithm.RSA256(publicKey, null) for asymmetric. Reject any token with alg:none before parsing. Added monitoring for tokens with unusual header structures. Rotated all secrets and forced password resets for affected users.- Never rely on library defaults for JWT validation. Explicitly specify allowed algorithms — and never include 'none'.
- JWT validation is not 'set and forget'. Test validation logic with malformed tokens in CI: alg:none, missing signature, expired timestamps.
- Monitor for tokens with unusual alg values. A token with alg:none is always an attack attempt — alert immediately.
- Use asymmetric signing (RS256) for microservices. The validation service only needs the public key; compromise doesn't leak the signing key.
setAllowedClockSkew(120) in JWT validation. Also check for multiple JWT validation libraries — they may have different leeway defaults.if (requestedUserId !== token.sub && !token.roles.includes('admin')) { return 403; }Access-Control-Allow-Origin header — never use * with credentials. For development, use specific origin https://app.example.com. For multiple origins, inspect the Origin header and echo it back if allowed, or use a regex whitelist. Add Access-Control-Allow-Credentials: true if using cookies.Key takeaways
Common mistakes to avoid
5 patternsAccepting JWT with alg:none due to library default
JWT.require(Algorithm.HMAC256(secret)) NOT JWT.require(algorithm) where algorithm comes from token header. Reject tokens with alg:none before parsing.Broken Object Level Authorization (BOLA)
/users/123/profile and change the ID to /users/456/profile to view another user's data. API validates JWT but never checks if requested ID belongs to the authenticated user.requestedUserId from path/query with token.sub claim. Only allow access if they match or if the authenticated user has admin role. Never rely on 'security through obscurity' of hard-to-guess IDs.In-memory rate limiting across multiple gateway instances
rate_limit:{userId}:{minute_window}. All gateway instances check the same Redis key. Test with multiple instances in staging before production.Trusting X-Forwarded-For headers for rate limiting without validation
X-Forwarded-For: 1.2.3.4, 5.6.7.8, 9.10.11.12 — the rate limiter keys on the client-supplied value, not the real IP. Attacker cycles through millions of fake IPs to bypass limits.X-Real-IP set by trusted proxy.String concatenation in SQL queries
' OR '1'='1 turns SELECT FROM users WHERE id = ' + userId + ' into SELECT FROM users WHERE id = '' OR '1'='1' — returns all users, bypassing authentication.Interview Questions on This Topic
Explain the difference between authentication and authorization — and give a real example where an API fails at one but not the other.
/users/456/profile to view Bob's data. AuthN succeeded (Alice is who she says she is), but AuthZ failed (Alice is not allowed to access Bob's data).Frequently Asked Questions
That's Security. Mark it forged?
3 min read · try the examples if you haven't