JWT alg:none Bypass — Why Your Token Validation Is Broken
A 72-hour breach from one JWT header field.
20+ years shipping large-scale distributed systems. Notes here come from systems that actually shipped.
- 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.
Why JWT alg:none Is a Critical Validation Gap
JWT alg:none bypass is a token validation flaw where an attacker crafts a JWT with the algorithm header set to 'none', tricking a server that fails to enforce algorithm verification into accepting unsigned tokens as valid. The core mechanic: the server's JWT library checks the signature only when the algorithm is explicitly set to a signing algorithm; if 'none' is allowed, the library skips signature verification entirely. This turns a stateless authentication token into a trivially forgeable credential.
In practice, this vulnerability arises when developers use JWT libraries with default configurations that accept 'none' or when custom validation logic checks the algorithm header after parsing but before verification. The attack works because the JWT specification requires servers to reject tokens with 'alg:none' unless the token is sent over a secure channel — a condition rarely met in web APIs. Attackers exploit this by sending a token with a modified header and payload, signed with nothing, and the server accepts it as authentic.
You must use this knowledge to audit every JWT validation pipeline: explicitly whitelist allowed algorithms (e.g., RS256, HS256) and reject any token with 'alg:none' or an unrecognized algorithm. This matters because a single misconfigured library or missing validation step can expose your entire API to unauthorized access — no brute-forcing required.
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.Role-Based Access Control (RBAC) — Stop Giving Everyone a Master Key
You wouldn't give a summer intern the root password to prod. But week after week I see APIs where every endpoint trusts the caller implicitly because "they passed auth." That's not security, that's a welcome mat.
RBAC is the principle of least privilege applied to API design. You define roles — admin, editor, viewer — and map them to explicit permission sets. Your API gateway or middleware checks not just "is this user authenticated?" but "does this user's role have the invoice:delete permission?" before allowing the operation.
The WHY is obvious: containment. If an attacker compromises a viewer account, they can't delete records. If a rogue admin goes wild, you revoke the role, not rebuild the database. Implement RBAC at the gateway layer, not in every controller. Centralize the policy decision point so you don't have twenty different auth checks scattered across services, each with their own bugs.
Use enum-based roles stored in your token or session. Never parse roles from user-supplied payloads — that's how you get privilege escalation straight out of OWASP's top ten.
API Gateways with WAF Integration — Your First Line of Defense Isn't Your Code
Your application code shouldn't be the first thing that sees a malicious request. That's like letting strangers walk through your front door before checking if they're carrying a crowbar. An API gateway with a Web Application Firewall (WAF) is your bouncer.
A WAF inspects incoming traffic at layer 7 — HTTP headers, query parameters, request bodies — and blocks patterns that match known attack signatures: SQL injection attempts, XSS payloads, path traversal, and mass assignment exploits. It's not a replacement for input validation; it's a pre-filter that eats the shotgun blast so your validation logic only sees clean rounds.
Integrate it with rate limiting at the gateway layer. Block an IP after 100 requests per second? That's the WAF's job. Correlate unusual request patterns — like a single client hitting every GET /api/users/{id} endpoint in sequence — and shunt them to a slow queue or drop them entirely. Your upstream services should never have to think about volumetric attacks.
The critical nuance: Don't treat your WAF rules as static. Attackers evolve. Subscribe to managed rule sets (AWS WAF Managed Rules, Cloudflare OWASP CRS) that auto-update. And for god's sake, log WAF blocks to a separate stream — you'll need them for post-mortem analysis when something inevitably slips through.
Regular Audits and Penetration Testing — Assume You're Already Breached
Your code is not safe. You have bugs. You have misconfigurations. That's not pessimism — that's engineering reality. The only question is whether you find them before someone else does. Regular audits and penetration testing flip the script: instead of hoping your defenses hold, you actively try to break them.
Penetration testing is not a checkbox exercise. It's a tactical recon mission. You pay experts to think like attackers — SQL injection, broken object-level authorization, JWT manipulation, race conditions. They will find things your unit tests never dreamed of. Combine annual third-party tests with quarterly internal red teams.
Automated scanning catches the low-hanging fruit: outdated libraries, exposed endpoints, missing headers. But automation misses business logic flaws. That's where manual testing earns its keep. Run audits after every major release. Document findings. Fix them before shipping. Treat every vulnerability as a production incident, because for your users, it is.
Best Practices for API Security — Stop Making the Same Mistakes
Security isn't a feature you bolt on in QA. It's a constraint you bake into your architecture from day one. Here are the non-negotiables: use HTTPS everywhere — not just login pages. Enforce TLS 1.2 minimum. Implement API keys for machine-to-machine traffic, and treat them like passwords: rotate quarterly, revoke immediately on compromise.
Validation is your first wall. Whitelist inputs, don't blacklist. Reject anything that doesn't match your schema. SQL injection is still the #1 attack vector because devs still concatenate queries. Stop that. Use parameterized queries or an ORM. For REST, enforce HTTP method semantics — GET never mutates state, POST creates, PUT replaces, DELETE destroys.
Log everything that's suspicious. 401s from unknown API keys. Rapid-fire requests. Requests hitting endpoints that don't exist. Ship those logs to a SIEM. Alert on anomalies. And please — never, ever hardcode secrets. Use environment variables. Use a vault. Your .env file is not production-ready.
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.grep -r 'Bearer' /var/log/api/* | grep -E 'alg.{0,5}none'echo 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIn0.' | jwt decode -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
20+ years shipping large-scale distributed systems. Notes here come from systems that actually shipped.
That's Security. Mark it forged?
8 min read · try the examples if you haven't