Senior 3 min · March 06, 2026

JWT alg:none Bypass — Why Your Token Validation Is Broken

A 72-hour breach from one JWT header field.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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
Plain-English First

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.

io/thecodeforge/api/JwtValidation.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package io.thecodeforge.api;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.security.interfaces.RSAPublicKey;

/**
 * Production JWT validation — explicit algorithms, no defaults, no alg:none.
 * The single most common security failure in API production incidents
 * is accepting tokens with alg:none due to library defaults.
 */
public class JwtValidation {

    // ─── SYMMETRIC KEY (HS256) — single secret shared between issuer and validator
    // Only use when issuer and validator are the same service.
    // Secret must be at least 32 bytes (256 bits) for HS256.
    public static DecodedJWT validateSymmetric(String token, String secret) {
        Algorithm algorithm = Algorithm.HMAC256(secret);
        JWTVerifier verifier = JWT.require(algorithm)
            .acceptLeeway(60)                    // 60 seconds clock skew tolerance
            .withIssuer("auth.thecodeforge.io")  // Restrict to trusted issuer
            .build();
        // Throws JWTVerificationException on failure — signature, expiry, issuer mismatch
        return verifier.verify(token);
    }

    // ─── ASYMMETRIC KEY (RS256) — public key for validation, private key for signing
    // Preferred for microservices: validation service only needs public key.
    // Compromised validation service cannot forge new tokens (needs private key).
    public static DecodedJWT validateAsymmetric(String token, RSAPublicKey publicKey) {
        Algorithm algorithm = Algorithm.RSA256(publicKey, null);
        JWTVerifier verifier = JWT.require(algorithm)
            .acceptLeeway(60)
            .withIssuer("auth.thecodeforge.io")
            .build();
        return verifier.verify(token);
    }

    // ─── EXPLICITLY REJECT ALG:NONE — the most dangerous JWT misconfiguration
    // Attackers send {"alg":"none","typ":"JWT"} with empty signature.
    // Libraries that don't explicitly reject it skip verification entirely.
    public static void rejectAlgNone(String tokenHeader) {
        if (tokenHeader.contains("\"alg\":\"none\"")) {
            throw new SecurityException("alg:none tokens are never accepted — possible attack");
        }
    }

    // ─── Extract claims after validation — never before!
    // Claims from an unverified token are attacker-controlled and MUST NOT be trusted.
    public static void processClaims(DecodedJWT verifiedToken) {
        String userId = verifiedToken.getSubject();
        String role = verifiedToken.getClaim("role").asString();
        // Only after verification — these claims are now trusted
        System.out.printf("Authenticated user: %s with role: %s%n", userId, role);
    }
}
The JWT alg:none Vulnerability — Still Working in 2026
The 'none' algorithm vulnerability was discovered in 2015. In 2026, libraries still accept it by default if you don't explicitly restrict algorithms. Attackers scan for this vulnerability constantly — it's the easiest way to bypass authentication completely. Always set allowed algorithms explicitly: JWT.require(Algorithm.HMAC256(secret)) NOT JWT.require(algorithm) with algorithm from the token header.
Production Insight
JWT validation without explicit algorithm restriction is a critical vulnerability. Attackers send alg:none and bypass signature checks entirely.
The library's default may include 'none'. Always specify allowed algorithms in your validation code.
Rule: Never trust JWT claims before signature verification. Validate first, then extract claims. An unverified token is attacker-controlled input.
Key Takeaway
Authentication verifies identity; authorization verifies permissions. Never confuse them.
JWT validation must explicitly reject alg:none and specify allowed algorithms — never rely on defaults.
Rule: Validate the JWT signature before extracting a single claim. Unverified claims are attacker-controlled input and cannot be trusted.
JWT Algorithm Selection
IfSingle service issues and validates tokens (monolith, BFF)
UseUse HS256 (symmetric). Store secret in secrets manager, rotate quarterly. Simpler, faster, no key management complexity.
IfMultiple microservices validate tokens, one auth service issues them
UseUse RS256 (asymmetric). Auth service holds private key; each validation service only needs public key. Compromised validation service can't forge tokens.
IfTokens must be validated by third-party services you don't control
UseUse RS256 with public key distribution via JWKS endpoint. Third party fetches public key from /.well-known/jwks.json.
IfTokens have large payloads (>4KB) causing header size issues
UseUse reference tokens (opaque string) stored in Redis with JWT as the reference. Trade-off: validation service must call auth service for each request.
IfYou need to revoke specific tokens before expiry
UseUse short-lived JWTs (5-15 minutes) plus refresh tokens. For revocation, maintain a denylist in Redis checked during validation.

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).

io/thecodeforge/api/RedisRateLimiter.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package io.thecodeforge.api;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

/**
 * Production distributed rate limiter using Redis with atomic INCR + EXPIRE.
 * This works correctly across multiple API gateway instances.
 *
 * Key pattern: rate_limit:{userId}:{minute_window}
 * Window granularity: 60 seconds aligned to wall clock minute.
 * Trade-off: fixed window allows double-burst at window boundaries.
 * For sliding window, use sorted sets (ZADD + ZREMRANGEBYSCORE).
 */
public class RedisRateLimiter {

    private final JedisPool jedisPool;
    private final int limitPerMinute;

    public RedisRateLimiter(String redisHost, int port, int limitPerMinute) {
        this.jedisPool = new JedisPool(redisHost, port);
        this.limitPerMinute = limitPerMinute;
    }

    /**
     * Check if request is allowed for this user.
     * Returns true if under limit, false if rate limited.
     */
    public boolean isAllowed(String userId) {
        // Fixed window aligned to current minute
        long currentMinute = System.currentTimeMillis() / 60000;
        String key = String.format("rate_limit:%s:%d", userId, currentMinute);

        try (Jedis jedis = jedisPool.getResource()) {
            // Atomic increment — returns the new count
            long currentCount = jedis.incr(key);

            // Set expiry on first increment only (60 seconds + 1 second buffer)
            if (currentCount == 1) {
                jedis.expire(key, 61);
            }

            return currentCount <= limitPerMinute;
        }
    }

    /**
     * Sliding window rate limiter using Redis sorted sets.
     * More accurate but higher memory and CPU overhead.
     * Use for strict rate limits where fixed window bursts are unacceptable.
     */
    public boolean isAllowedSlidingWindow(String userId, int limitPerMinute) {
        String key = String.format("rate_limit_sliding:%s", userId);
        long nowMs = System.currentTimeMillis();
        long windowStartMs = nowMs - 60000;  // 60 seconds ago

        try (Jedis jedis = jedisPool.getResource()) {
            // Remove requests older than 60 seconds
            jedis.zremrangeByScore(key, 0, windowStartMs);

            // Count requests in current window
            long requestCount = jedis.zcard(key);

            if (requestCount >= limitPerMinute) {
                return false;  // Rate limited
            }

            // Add current request with timestamp as score
            jedis.zadd(key, nowMs, String.format("%d:%s", nowMs, userId + System.nanoTime()));
            jedis.expire(key, 61);  // Auto-cleanup

            return true;
        }
    }
}
Rate Limiting vs Throttling
Rate limiting rejects requests that exceed the limit (returns 429). Throttling queues or slows down requests instead of rejecting them. Use rate limiting for public APIs to prevent abuse. Use throttling for internal services where occasional latency spikes are better than dropping requests entirely. Throttling requires more careful design to avoid queue buildup.
Production Insight
In-memory rate limiters fail silently in distributed environments. Each gateway instance has its own counter, so users get effectively multiplied limits.
Redis with atomic INCR + EXPIRE is the standard solution for shared rate limit state.
Rule: If you have more than one gateway instance (which you must for HA), your rate limiter requires a shared store. Add Redis before you scale, not after.
Key Takeaway
Rate limiting without distributed state is a false sense of security. Two gateway instances = double the limit.
Redis INCR + EXPIRE with atomic operations is the production standard for shared rate limit state.
Rule: Key rate limits on user ID or API key, not IP address — attackers can spread across IPs to bypass IP-based limits.
Rate Limiting Strategy Selection
IfSimple use case, single gateway instance, low traffic (< 10k RPS)
UseFixed window in-memory counter. No external dependencies. Budget = limit per window, resets on the minute.
IfDistributed gateways (HA or scaling), higher traffic
UseFixed window with Redis INCR + EXPIRE. Atomic, shared across instances. Trade-off: double bursts at window boundaries.
IfStrict limits where window-boundary bursts are unacceptable (financial, regulatory)
UseSliding window with Redis sorted sets (ZADD + ZREMRANGEBYSCORE). Higher overhead but exact.
IfNeed to allow bursts while maintaining average rate
UseToken bucket with Redis storing tokens and last_refill_timestamp. Burst size = bucket capacity.
IfDistributed attackers using multiple IPs (botnets)
UseKey on user ID or API key, not IP. For unauthenticated endpoints, use fingerprinting (combined IP + User-Agent) + challenge CAPTCHA on high request rates.

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.

io/thecodeforge/api/InputValidation.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package io.thecodeforge.api;

import java.util.regex.Pattern;
import java.util.UUID;

/**
 * Production input validation — validate early, validate strictly.
 * All input from clients is attacker-controlled until proven otherwise.
 *
 * Key principles:
 *   1. Validate type, length, format, and range
 *   2. Use allowlists (known-good patterns), not denylists (known-bad)
 *   3. Validate before authentication to reject junk early
 *   4. Parameterize queries — validation is NOT a replacement for prepared statements
 */
public class InputValidation {

    // ─── ALLOWLIST PATTERNS — explicit, strict, minimal ──────────────────────
    private static final Pattern UUID_PATTERN =
        Pattern.compile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", Pattern.CASE_INSENSITIVE);

    private static final Pattern USERNAME_PATTERN =
        Pattern.compile("^[a-zA-Z0-9_]{3,20}$");  // Alphanumeric + underscore, 3-20 chars

    private static final Pattern EMAIL_PATTERN =
        Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");

    private static final Pattern ORDER_ID_PATTERN =
        Pattern.compile("^ORD-[0-9]{10,15}$");    // ORD-1234567890

    // ─── Input validation methods — early returns, clear error messages ──────
    public static boolean isValidUUID(String value) {
        if (value == null) return false;
        return UUID_PATTERN.matcher(value).matches();
    }

    public static boolean isValidUsername(String value) {
        if (value == null) return false;
        return USERNAME_PATTERN.matcher(value).matches();
    }

    // ─── LENGTH VALIDATION — always enforce maximums, even for legitimate fields
    // Email spec allows 254 characters total. Most legitimate emails are under 50.
    // Reject excessively long inputs early to prevent resource exhaustion.
    public static String validateAndSanitizeEmail(String input) {
        if (input == null || input.length() > 254) {
            throw new IllegalArgumentException("Invalid email format or length");
        }
        String trimmed = input.trim();
        if (!EMAIL_PATTERN.matcher(trimmed).matches()) {
            throw new IllegalArgumentException("Invalid email format");
        }
        return trimmed;
    }

    // ─── ENUM VALIDATION — reject anything not in the explicit set
    public enum OrderStatus { PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED }

    public static OrderStatus validateOrderStatus(String input) {
        if (input == null) throw new IllegalArgumentException("Status is required");
        try {
            return OrderStatus.valueOf(input.toUpperCase());
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException("Invalid status: " + input +
                ". Allowed values: PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED");
        }
    }

    // ─── PARAMETERIZED QUERY — the only real defense against SQL injection
    // Input validation catches obviously malicious input, but parameterization
    // is what makes injection structurally impossible.
    public static void parameterizedQueryExample() {
        // NEVER do this:
        // String query = "SELECT * FROM users WHERE id = '" + userId + "'";

        // ALWAYS do this:
        // PreparedStatement stmt = connection.prepareStatement(
        //     "SELECT * FROM users WHERE id = ?"
        // );
        // stmt.setString(1, userId);
    }
}
Validate Before Authentication
Run input validation BEFORE authentication checks. A malformed request that fails validation shouldn't waste your auth service's resources. Attackers will probe with thousands of malformed requests — rejecting them at the validation layer saves significant compute. The only exception: validating authentication credentials themselves (username/password format) must happen after auth, but payload validation happens before.
Production Insight
Input validation is not a replacement for parameterized queries. Attackers can bypass validation but parameterization stops them structurally.
Never concatenate user input into SQL, NoSQL, or shell commands — use parameterized APIs or an object mapper.
Rule: Validate for safety (reject malicious patterns), parameterize for security (make injection impossible). Both are necessary; neither alone is sufficient.
Key Takeaway
Input validation rejects malicious input early — the fence at the top of the cliff.
Parameterized queries make SQL injection structurally impossible — the safety net below.
Rule: Validate type, length, format, and range. Use allowlists, not denylists. Attackers always find new bad patterns.
Input Validation Strategy
IfField type is UUID, enum, or has fixed format
UseUse allowlist regex validation at API boundary. Reject before hitting business logic. Return 400 with clear error.
IfField is free text (user bio, comment, review)
UseValidate maximum length only (e.g., 10,000 chars). Escape on output for HTML display. Do NOT parse or interpret content.
IfInput is SQL parameter (ID, name for WHERE clause)
UseValidate type and length, then use parameterized query. Validation alone is insufficient — parameterization prevents injection.
IfInput is used in system command or file path
UseAvoid entirely. If unavoidable, validate with extreme strict allowlist (alphanumeric only) and use allowlist of safe commands.
IfInput includes JSON, XML, or serialized objects
UseParse with secure parser that limits depth, size, and prevents billion-laughs attacks. Use maxDepth and maxStringLength constraints.
● Production incidentPOST-MORTEMseverity: high

The JWT alg:none Exploit That Bypassed Authentication

Symptom
API started returning sensitive data for requests that appeared unauthenticated. Logs showed requests with malformed JWTs succeeding. No signature verification failures in logs. A junior engineer noticed that changing a single character in the token still passed validation.
Assumption
The team assumed their JWT library verified signatures by default. They didn't know that some libraries accept the 'none' algorithm for backward compatibility, and they never explicitly restricted allowed algorithms. The default configuration allowed 'none' alongside 'HS256' and 'RS256'.
Root cause
The JWT validation code used a generic library method that accepted any algorithm. An attacker sent a token with {"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.
Fix
Explicitly set allowed algorithms in JWT validation: 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.
Key lesson
  • 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.
Production debug guideSymptom → Action mapping for common API security failures5 entries
Symptom · 01
401 Unauthorized for requests that should be authenticated — but only sometimes
Fix
Check JWT expiration skew. If your server time is ahead of the authorization server's time by > leeway (default 60 seconds), tokens validated as expired. Set setAllowedClockSkew(120) in JWT validation. Also check for multiple JWT validation libraries — they may have different leeway defaults.
Symptom · 02
User can access another user's data by changing ID in URL —/users/123/data accessed by user 456
Fix
Broken Object Level Authorization (BOLA). Your API validates the JWT but never checks if the authenticated user owns or is permitted to access the requested resource ID. Add a middleware that enforces ownership: if (requestedUserId !== token.sub && !token.roles.includes('admin')) { return 403; }
Symptom · 03
Rate limits not applying — user making 1000 req/min when limit is 100
Fix
Check if rate limiter state is shared across instances (use Redis). Verify key includes user ID or client ID, not just IP (attackers behind NAT share IP). Check if limit is per endpoint or global — a user can spread 1000 requests across 10 endpoints, each with limit 100, and never trigger any single limit. Use aggregate rate limiting.
Symptom · 04
SQL injection possible — input with single quote causes database error
Fix
Your API is concatenating user input into SQL queries. Stop using string concatenation immediately. Use parameterized queries (prepared statements) or an ORM that escapes automatically. Run a SAST tool to find all raw SQL concatenation patterns. Add WAF rule that blocks requests with SQL metacharacters as a temporary mitigation.
Symptom · 05
CORS errors — browser can't call API from allowed frontend domain
Fix
CORS is misconfigured. Check 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.
★ API Security Debug Cheat SheetFast diagnostics for production API security issues. Run these at the first sign of a breach.
Suspected JWT alg:none attack
Immediate action
Check logs for tokens with unusual JWT headers or missing signatures
Commands
grep -r 'Bearer' /var/log/api/* | grep -E 'alg.{0,5}none'
echo 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIn0.' | jwt decode -
Fix now
Update JWT validation to explicitly reject alg:none. Rotate all secrets. Force logout all users. Add monitoring for alg:none in WAF.
User accessing another user's data via ID parameter+
Immediate action
Check if API validates ownership of requested resource against authenticated user ID
Commands
curl -H 'Authorization: Bearer user123-token' https://api.example.com/users/456/profile
grep -r 'req.params.id\|req.query.userId' routes/ | head -10
Fix now
Add middleware that compares requestedUserId with token.sub before allowing access. Reject with 403 Forbidden if mismatch and user is not admin.
SQL injection suspected — single quote in input triggers database error+
Immediate action
Check if API uses parameterized queries or string concatenation for SQL
Commands
curl -X POST https://api.example.com/search -d '{"query":"test'"}' -H 'Content-Type: application/json'
grep -r 'execute\b.*\+.*req\.' services/
Fix now
Stop using string concatenation for SQL. Replace with prepared statements: db.query('SELECT * FROM users WHERE id = $1', [userId]). Add WAF rule blocking SQL metacharacters as temporary mitigation.
Rate limit bypassed — user making 3x limit+
Immediate action
Check if multiple gateway instances share rate limit state via Redis
Commands
kubectl get pods -l app=gateway | wc -l
redis-cli KEYS 'rate_limit:*' | wc -l
Fix now
If gateway instances >1 and Redis keys missing, rate limiter is in-memory per instance. Deploy Redis and switch to atomic INCR+EXPIRE operations.
API returning data over HTTP (not HTTPS) in production+
Immediate action
Check if TLS is configured and enforced for all endpoints
Commands
curl -I http://api.example.com/health 2>&1 | head -5
ss -tln | grep :443
Fix now
Configure TLS certificate (Let's Encrypt, cloud provider). Add HSTS header: Strict-Transport-Security: max-age=31536000; includeSubDomains. Redirect all HTTP to HTTPS at load balancer level.
Authentication vs Authorization
AspectAuthentication (AuthN)Authorization (AuthZ)
PurposeVerifies identity — who is making the requestVerifies permissions — what they are allowed to do
Question answered"Are you who you say you are?""Are you allowed to do this?"
Typical mechanismJWT validation, OAuth token, API key, username/passwordRole-Based Access Control (RBAC), Access Control Lists (ACL), Policy-Based (ABAC)
Where it runsAPI gateway or auth middleware — once per requestPer service or per endpoint — after authentication
Failure consequence401 Unauthorized — request rejected, cannot proceed403 Forbidden — authenticated but not allowed
Common breachalg:none JWT bypass — attacker forges identityBOLA (Broken Object Level Authorization) — user accesses another user's data
Production checkVerify signature, exp, issuer, algorithm — reject alg:noneEnforce ownership: does token.sub own requested resource ID?

Key takeaways

1
API security is defense in depth
authentication, authorization, rate limiting, input validation, TLS, and monitoring. No single control is sufficient; the combination creates resilience.
2
JWT validation must explicitly reject alg:none and specify allowed algorithms
never rely on library defaults. Attackers scan for this vulnerability constantly.
3
Rate limiting without distributed state fails as soon as you have multiple gateway instances. Redis with atomic INCR + EXPIRE is the production standard for shared state.
4
Input validation is necessary but not sufficient for SQL injection. Parameterized queries make injection structurally impossible
use both.
5
Broken Object Level Authorization is the #1 OWASP API risk. Always verify that the authenticated user owns or is permitted to access the requested resource ID.

Common mistakes to avoid

5 patterns
×

Accepting JWT with alg:none due to library default

Symptom
Attackers send crafted tokens with {"alg":"none"} header and empty signature. API accepts them as valid. Any user can impersonate any other user, including admin.
Fix
Explicitly set allowed algorithms in JWT validation: 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)

Symptom
Authenticated user can access /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.
Fix
Add middleware that compares 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

Symptom
Rate limit is 100 req/min per user. With 2 gateway instances, user actually gets 200 req/min (100 per instance). Rate limiter never triggers because each instance stays under its own limit.
Fix
Move rate limit state to Redis with atomic INCR + EXPIRE. Key format: 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

Symptom
Attacker sends 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.
Fix
Rate limit on user ID or API key, not IP. For unauthenticated endpoints, take the first trusted IP from the load balancer (configured to overwrite X-Forwarded-For), not the client-supplied value. Use X-Real-IP set by trusted proxy.
×

String concatenation in SQL queries

Symptom
Input ' OR '1'='1 turns SELECT FROM users WHERE id = ' + userId + ' into SELECT FROM users WHERE id = '' OR '1'='1' — returns all users, bypassing authentication.
Fix
Use parameterized queries (prepared statements) or an ORM. Never concatenate user input into SQL, NoSQL, or command strings. Parameterization separates code from data, making injection structurally impossible.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the difference between authentication and authorization — and gi...
Q02SENIOR
Walk me through how you would secure a REST API that accepts user-genera...
Q03SENIOR
You discover that your API is vulnerable to injection attacks. What imme...
Q04SENIOR
Compare JWT with opaque reference tokens. When would you choose one over...
Q01 of 04SENIOR

Explain the difference between authentication and authorization — and give a real example where an API fails at one but not the other.

ANSWER
Authentication verifies identity — are you who you say you are? Authorization verifies permissions — are you allowed to do this? Example of failing AuthN but having correct AuthZ: A JWT with alg:none bypasses signature verification. The attacker can set any user ID in the payload. The API's authorization logic (checking roles) runs correctly but on attacker-controlled claims — the system authenticates no one but authorizes the attacker as if they were admin. Example of correct AuthN but failing AuthZ: BOLA vulnerability — user Alice authenticates with her own JWT, but the API allows her to change the ID in /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).
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What's the difference between JWT and OAuth 2.0?
02
Should I use HTTP Basic Auth for APIs?
03
How do I test if my API is vulnerable to BOLA (Broken Object Level Authorization)?
04
What is the difference between rate limiting and DDoS protection?
🔥

That's Security. Mark it forged?

3 min read · try the examples if you haven't

Previous
HTTPS and TLS Explained
4 / 10 · Security
Next
CSRF and XSS Prevention