# OWASP JWT: brute-force 10,000 secrets + kid injection

> Online JWT token audit free: alg=none, brute-force on 10,000 known HS256 secrets, alg confusion, missing claims, kid injection, unpinned jku/x5u. No upload.

Free in-browser tool (no upload): https://www.mauriziofonte.it/tools/jwt-security-audit

Paste a **JWT token** and get a security-first audit in seconds: detection of **alg=none** and permutations (`NoNe`, `NULL`, numeric alg), **brute-force** of an HS256 secret against a 10,000-secret dataset (wallarm/jwt-secrets + SecLists, recompiled nightly), **alg confusion attack** (HS256 signed with the backend's RSA pubkey) when you provide the public key, missing RFC 7519 claims (`exp`, `iat`, `aud`, `iss`, `jti`), suspicious lifetime, `kid` with injection-prone characters, unpinned `jku` and `x5u`, esoteric headers (`cty`, `zip`) used as pivots. Analysis runs entirely in the browser via WebCrypto: the token never leaves the page.

## How to use the audit

1. **Get a JWT** - Typical flow: capture the token from the browser HTTP request (DevTools > Network > `Authorization: Bearer...`), or from application logs, or directly from your identity provider during testing. For assisted analysis: save the token to a `.jwt` or `.txt` file and use the dropzone to upload it.
2. **Paste the token (and optionally the pubkey)** - Three parts separated by dots: `header.payload.signature`. The tool validates the base64url format and fails with a diagnostic message if any segment is malformed. To test the **alg confusion attack**: open the "Optional pubkey" section and paste the backend's RSA public key (PEM or JWKS). Without a pubkey the alg confusion check is skipped; the other 15+ checks still run.
3. **Run the audit** - **Audit** button for the default check (1,000 most common secrets from wallarm/SecLists, <1 second). **Deep audit** button to extend brute-force to 10,000 secrets (5-10 seconds, typical when the fast check found no match and you want to rule out the vector with broader coverage).
4. **Read the report** - Three-tier output. **Decoded header and payload** at the top (full text for manual claim inspection). **Findings ordered by severity**: critical, high, medium, low, info. **Aggregated score** 0-100. For each issue: technical description of the vector, normative reference (CVE, RFC, OWASP), suggested remediation.
5. **Export or print** - **Copy report**: plain text for internal tickets or audit documents. **Export for chatbot**: complete markdown with tool context, detailed findings, and prompt template, to paste into ChatGPT/Gemini/Claude for remediation prioritization or fix-effort estimates. Browser **Print** for PDF export (CSS print-optimized, hides nav and CTA).

## JWT attack vectors and why static triage matters

**The real risk in JWTs lies in configuration.** Reading the payload of a token (what the [JWT decoder](/tools/jwt-decoder) does) is the easy part: asking whether that token is exposed to one of the known compromise patterns separates a production-grade integration from an incident waiting to happen. Historical CVEs on widely-used JWT libraries (jsonwebtoken < 8.5, jose < 4, python-jwt, ruby-jwt) all stem from implicit backend assumptions that this tool surfaces from the token's manifest alone.

**The four classical vectors verified by the audit.** (1) **alg=none and permutations**: historically vulnerable libraries accepted `{"alg":"none"}` bypassing signature verification. The check covers the exact string and all case-mix permutations (`None`, `NONE`, `nULL`, `null`, empty alg, numeric alg, missing alg). (2) **HS256 with weak secret**: HMAC with a shared secret is brute-forceable offline if the secret is a human passphrase. The audit tries a curated dataset of 10,000 known secrets (wallarm/jwt-secrets + SecLists/scraped-JWT-secrets, recompiled nightly), verifying each candidate via local WebCrypto HMAC-SHA256. (3) **alg confusion attack**: the backend is configured for RS256 but also accepts HS256, and uses the RSA public key as the HMAC secret. Documented vector in historical CVEs (jsonwebtoken pre-8.5). If you provide the public key, the tool tries HMAC verification using the pubkey as secret and reports **critical** if it passes: it means the backend would accept attacker-controlled tokens signed with its own public key (which is public by definition). (4) **kid injection / jku-x5u abuse**: headers that declare which key to use for verification. If the backend doesn't sanitize them (kid used as key path or SQL lookup) or doesn't pin them to trusted domains (jku/x5u), an attacker who controls the input controls the key.

**Missing claims.** RFC 7519 defines optional claims with concrete security value: `exp` (expiration, time bound), `nbf` (not before, activation delay), `iat` (issued at, emission timestamp), `jti` (JWT ID, anti-replay), `aud` (audience, mitigation against confused deputy), `iss` (issuer, mitigation against rogue IdP). Frequent anti-pattern: tokens without `exp`, living forever with no server-side revocation; or without `aud`, where service A can forward the token to service B that accepts it because they share the same identity provider, even if it wasn't intended for B.

**Esoteric headers.** Two lesser-known headers the tool flags as info: `cty` (content type, enables nested JWT - a JWT inside a JWT, a confusion vector if the backend unwraps without re-authenticating), `zip` (compression: the header `{"zip":"DEF"}` enables DEFLATE decompression of the payload, and has been a vector for zip-bomb attacks against libraries that didn't limit output size).

**Operational privacy.** A JWT token contains the user-claim payload and the signature configuration in the header: in real audits you don't want to upload it to a third-party service, even over TLS. Here no network request leaves with token data. The secrets dataset (10,000 entries, ~180 KB raw) is downloaded by the browser only on first use and cached in `localStorage` for subsequent sessions (automatic invalidation via SHA-256 over the public file). Audit, JWT parsing, HMAC verification, secret brute-force, pubkey reading: all local via the WebCrypto API.

**When static triage is the right level.** Typical scenarios. (a) Preliminary review of a legacy API without backend access: you captured a token from the frontend and want to know if the server-side configuration is solid. (b) Pre-engagement audit to estimate the cost of a possible JWT hardening contract before quoting. (c) Initial screen before a serious cyber assessment: the high/critical list deserves deeper exploitability assessment, mapping of verify codepaths in the backend, verification of the algorithm whitelist and JWKS pinning. (d) Periodic self-check by the API owner who wants to know whether the integration was done properly.

**Scope limit.** The audit does not replace: an architectural review of the full flow (key rotation, refresh token rotation, server-side logout with jti blacklist, audience binding, kid and jku/x5u handling in production, OIDC provider configuration); static analysis of the verify code in the backend (algorithm whitelist, `exp` leeway, exception handling); the application-specific threat model. It is a triage and presents itself as one.

## Glossary

- **JWT**: JSON Web Token, RFC 7519. String formatted as `header.payload.signature`, each segment base64url-encoded. Contains JSON claims about identity or authorization. De facto standard for modern API authentication (OAuth2, OpenID Connect).
- **JWS / JWE**: **JWS** (JSON Web Signature, RFC 7515) = signed JWT, base64url payload in clear. **JWE** (JSON Web Encryption, RFC 7516) = encrypted JWT, 5 segments instead of 3, payload not readable without the key. This audit handles classical JWS (signed, payload readable).
- **alg=none**: Value of the `alg` header indicating no signature. RFC 7518 sec. 3.6 defines it as an option, but OWASP recommends rejecting it at the application level to prevent signature bypass. Vector in historical CVEs on jsonwebtoken, jose, ruby-jwt and other libraries.
- **Alg confusion attack**: Vector where the backend, configured for RS256, also accepts HS256 and uses the RSA public key as the HMAC secret. The attacker who has the pubkey (always, because public) can forge valid tokens. Mitigation: constrain the algorithm in verification (e.g. `algorithms: ['RS256']`).
- **HS256**: HMAC-SHA256, symmetric signature with a secret shared between issuer and verifier. The secret must be random and high-entropy (>= 256 bits, generated with `openssl rand -base64 32`) to resist offline brute-force.
- **RS256**: RSA-SHA256 with PKCS#1 v1.5 padding, asymmetric signature. The backend has the private key to sign, clients (or other services) have only the public to verify. Preferred over HS256 for multi-service architectures. RS384 and RS512 with SHA-384/SHA-512 also exist.
- **ES256 / ES384 / ES512**: ECDSA with P-256, P-384, P-521 curves. Equivalent to RS256 but with shorter keys (256 bits vs 2048-4096) and faster signatures. Recommended for new systems. Caveat: ECDSA with weak randomness is catastrophic.
- **PS256**: RSASSA-PSS-SHA256, RSA with PSS padding (probabilistic signature scheme). Modern variant of RS256, recommended by NIST. Supported by recent JWT libraries, less historically common.
- **kid**: Key ID, optional header that identifies which key to use for verification (useful in multi-key JWKS). Known vulnerabilities if not sanitized backend-side (path traversal with `../`, SQL injection on the keys table).
- **jku / x5u**: **jku** (JWK Set URL): URL of a JWKS. **x5u** (X.509 URL): URL of an X.509 certificate. Headers declaring where to find the verification key. Without backend pinning, the attacker who controls the URL controls the key. Ignore unless explicitly whitelisted.
- **JWKS**: JSON Web Key Set (RFC 7517). Public endpoint that publishes the public keys of an identity provider in JSON format. Conventional URL: `https://idp.example/.well-known/jwks.json`. Cached with reasonable TTL, refreshed on rotation.
- **exp / nbf / iat**: Temporal claims RFC 7519. `exp` (expiration): Unix expiration timestamp in seconds. `nbf` (not before): timestamp before which the token is invalid. `iat` (issued at): emission timestamp. All in seconds (not milliseconds: a common JS porting bug).
- **aud / iss / jti**: Semantic claims RFC 7519. `aud` (audience): recipient identifier, mitigation against confused deputy. `iss` (issuer): identifier of the provider that issued the token, mitigation against rogue IdP. `jti` (JWT ID): unique identifier, enables anti-replay via server-side blacklist.
- **OWASP JWT Cheatsheet**: OWASP document codifying JWT security best practices: algorithm whitelist, secret strength, mandatory claims, expiration, kid handling, jku/x5u pinning, refresh token rotation. Authoritative reference for anyone designing or auditing a JWT integration.
- **FriendsOfPHP / SecLists**: **SecLists** (danielmiessler/SecLists on GitHub): open source collection of wordlists for security testing. The Passwords/scraped-JWT-secrets section contains common JWT secrets scraped from CTFs, leaks, public code. Source of this tool's brute-force dataset alongside **wallarm/jwt-secrets**.
- **WebCrypto API**: Browser standard API for cryptographic primitives (HMAC, RSA, ECDSA, AES, SHA). Defined in W3C Web Cryptography. All HMAC and RSA verification in this tool runs via `crypto.subtle`: no external JS library, native performance, no data leaves the tab.

## Frequently asked questions about the JWT audit

### I'm the owner or PM of an API that uses JWT, not a developer. Do I really need this tool?

Yes, and you are in fact one of the targets. For each finding you get severity (critical, high, medium, low, info), description of the vector in operational language, and link to the public CVE or OWASP recommendation. If the report returns even a single critical finding, you have a concrete argument to bring to the development team and ask how and when the integration will be fixed. Typical alarm signal: HS256 secret found in the wallarm dataset = secret among the 10,000 most diffused on the web = patch needed within 24 hours.

### Where do I capture a JWT from my application to test it?

Three typical paths. (1) Browser DevTools: Network tab, filter for API requests, look for the `Authorization: Bearer ...` header, copy everything after "Bearer ". (2) Client storage: the JWT is often in `localStorage` (key like `access_token`, `idToken`) or in a cookie. (3) Developer: ask the team for a test token (not production on a real user: prefer a staging account). The tool runs locally, the token never leaves the browser, but as hygiene never paste real tokens in tools you do not control.

### Does the tool decode the signature too? Can I see it?

The third segment (signature) is shown as a base64url string, not decoded. HMAC, RSA and ECDSA signatures are raw bytes and reading them adds no useful information. To verify it you need the key: for HS256 use the [JWT decoder](/tools/jwt-decoder) with the secret in the dedicated field; for RS256/ES256 you can paste the pubkey in the optional field of this audit, and if the token is HS256-spoofed with the pubkey as secret the alg confusion check will report it.

### What happens if the tool finds a weak secret? What does it mean in practice?

It means anyone who knows or guesses that secret can forge valid tokens at will, impersonating any user. Immediate mitigation pattern: (1) rotate the secret to a crypto-safe random string (`openssl rand -base64 32`); (2) invalidate all tokens in circulation (force re-login for all users, unpleasant but necessary); (3) audit application logs from the past weeks for suspicious activity on privileged accounts; (4) reconsider the architecture: whether HS256 is truly the right choice or if RS256 would be more appropriate.

### What exactly is the alg confusion attack? Is the check reliable?

The attack exploits misconfigured backends that, on receiving an HS256 token, verify it with the same key used for RS256, applying the algorithm declared in the token header (HS256) instead of the one expected by the architecture (RS256). Result: the backend treats the RSA public key as an HMAC secret, and since the public key is public, the attacker can forge valid tokens. The tool's check is 100% reliable: it takes the pubkey you provide, uses it as HMAC secret against the token's signature, and if verification passes the vector is confirmed. Without a pubkey, the check is skipped.

### Deep audit (10,000 secrets) vs Audit (1,000): when is it really needed?

The fast tier (1,000 secrets) covers 95% of real cases: common secrets ("secret", "password", "changeme", combinations with project names) are all up top. The full tier (1,001-10,000) adds the long tail of less frequent but documented secrets from CTFs, GitHub leaks, historical pastebins. When to use deep: (a) the fast check found no match and you want to rule out the vector with broader coverage before closing the triage; (b) high-rigor audit before publishing an official report; (c) the backend is in NIS2 scope and you must document due diligence.

### Expired token: is it a blocking finding?

No, it's classified as INFO. An expired token is NOT a security vulnerability (in fact, expiration is an active mitigation). The tool reports the state to avoid false positives in the report (an audit saying "missing exp" on a token that actually has exp but is expired would be a bug).

### Is lifetime > 30 days really a problem?

Depends on the use case. For user session tokens: yes, 30 days is too much, refresh token rotation with short-lived (15-60 min) access tokens is the recommended practice. For service tokens (JWT-format API keys): 30+ days may be acceptable if rotated and revocable backend-side. The tool flags HIGH as a conservative default: evaluate context and ignore if the use case justifies it.

### kid injection: how is it actually exploited?

Real-world examples. (1) `kid: "../../../dev/null"` -> backend looks up the key in the filesystem -> finds an empty file -> verifies HMAC with secret = empty string -> forgery succeeds. (2) `kid: "key1' UNION SELECT 'attacker'-- "` -> backend runs SQL on keys table -> SQLi -> attacker-controlled key. Mitigation: treat `kid` as untrusted input, alphanumeric whitelist, exact lookup in an internal map, never as an unparameterized query argument.

### Why are jku/x5u declared high risk?

They allow the token to declare the URL of the public key with which to verify itself. If the backend uses it as-is (without pinning to trusted domains), an attacker who controls the URL controls the key -> valid signatures at will. Mitigation: hostname whitelist pinning at the backend level, or ignore `jku`/`x5u` entirely and use only a hardcoded JWKS endpoint.

### Does the tool integrate any external PHP/JS library at runtime?

No. JWT parsing (base64url + JSON.parse), HS256 brute-force (WebCrypto HMAC.verify), pubkey PEM import (WebCrypto importKey with SubjectPublicKeyInfo): all native browser APIs. Zero dependencies, ~16 KB total of JavaScript. The secrets dataset is static, recompiled by the server-side PHP cron.

### Difference between this audit and the JWT decoder on the site?

The [JWT decoder](/tools/jwt-decoder) is a viewer: decodes header and payload, explains each claim, computes the exp countdown, optionally verifies the HS256 signature if you give it the secret. This audit assumes the signature is valid (or verifies it on the fly for the alg confusion check) and focuses on configuration risks: forgery vectors, weak secrets, missing claims, dangerous headers. They are complementary: decoder to understand what a token contains, audit to understand whether it's exposed.

### Privacy: is the pasted token stored or tracked?

No. The tool runs entirely in the browser. The only outgoing network requests are GETs of the static secrets dataset files (identical for every user, fully anonymous) and the `index.json` GET. The pasted token is never sent in any request, never saved to `localStorage`, never tracked in analytics. Verifiable by opening the DevTools Network panel during the audit: zero outgoing requests with token data.

### Can I audit a Refresh Token or an OpenID Connect ID Token?

Yes, syntactically both are valid JWTs and the audit works the same. Relevant points for refresh tokens: (a) `exp` should be reasonably long (days-months typical) but with jti for anti-replay; (b) `aud` restricted to the refresh endpoint; (c) ideally HS256 replaced by RS256 in multi-service architectures. For OIDC ID tokens: `iss` must match the OIDC provider URL, `aud` must contain the client_id, `nonce` must match the one sent in the authorization request.

### Can I do a full flow audit, not just a single token?

This audit reports risks visible from a single token. To examine the full flow (key rotation, refresh token rotation, server-side logout, anti-replay with jti, audience binding, kid and jku/x5u handling in production, OIDC provider configuration) you need an architectural review covering code, provider configuration and infrastructure. See the CTA at the bottom of the page.

---
Tool by Maurizio Fonte, senior backend engineer. All tools: https://www.mauriziofonte.it/tools/
