Skip to content
Token security

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

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.

Drop a .jwt or .txt file containing the token here, or click to select (max 5 MB)
Optional pubkey for alg confusion check

If the token is HS256 and you provide the backend's RSA public key, the tool attempts HMAC verification using the pubkey as a secret. If it passes, you have a critical alg confusion finding: the backend would accept HS256 tokens signed with its own public key. Leave empty to skip this check.

How to use the audit

  1. 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. 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. 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. 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. 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 comes from configuration, not from the format. Reading the payload of a token (what the JWT decoder does) is the easy part: asking whether that token is exposed to one of the known compromise patterns is what 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.

What the tool checks, in detail

The audit applies 18 checks across 4 families. All checks run locally in the browser via WebCrypto (for HMAC verifications) and pure JSON parsing for the rest.

Family 1 - Algorithm (4 checks). alg_none verifies the exact string none in any case. alg_permutation covers malicious variants: empty alg, numeric alg, JSON null, NULL/None/NoNe/nULL. alg_confusion_confirmed (requires optional pubkey): if the token is HS256 and the user provided an RSA public key, the tool attempts HMAC-SHA256 verification using the pubkey as secret. If verification passes, the backend accepts HS256 tokens signed with its own public key. Vector confirmed. weak_secret: Audit button tests against the fast tier (1,000 common secrets, <1s); Deep audit button tests against the full tier (10,000 additional secrets, 5-10s).

Family 2 - Header esoterics (4 checks). kid_suspicious: the kid value contains characters typical of path traversal (../) or SQLi (UNION, SELECT, OR, AND, --, '). jku_present and x5u_present: headers declaring an external public key URL; without whitelist pinning, the attacker who controls the URL controls the key (HIGH). typ_missing: absence of typ, recommended by RFC 7519 sec. 5.1. cty_present and zip_present: headers enabling nested JWT and DEFLATE, flagged INFO for manual codepath inspection.

Family 3 - Payload claims (8 checks). For each relevant RFC 7519 claim: presence, format, value. exp_missing: token without expiration (HIGH). exp_expired: expiration in the past (INFO, active mitigation). exp_far_future: exp > 1 year in the future (MEDIUM). iat_missing: missing issued-at (LOW). lifetime_too_long: exp - iat > 30 days (HIGH, configurable). aud_missing: no audience binding (MEDIUM, confused deputy). iss_missing: no issuer (MEDIUM, rogue IdP). jti_missing: no anti-replay ID (LOW).

Family 4 - HS256 brute-force (1 check, 2 modes). Implementation: the browser imports each candidate via crypto.subtle.importKey as an HMAC key, then verifies the header.payload segment signature via crypto.subtle.verify against the token's signature. Sequential (HMAC-SHA256 on small input does not benefit from browser parallelism). Typical throughput: 1,000-2,000 verifies/second on a modern laptop. Fast tier 1,000 secrets = sub-second. Full tier 10,000 secrets = 5-10 seconds. The dataset is recompiled nightly by shared/cron/build_jwt_secrets_db.php via union(wallarm, SecLists), case-insensitive dedup, capped at 10,000 entries (the 90,000+ long tail is dropped: after the first 10k the additional match rate is marginal and browser cost doesn't justify it).

Atomic publication. The cron job applies an atomic multi-file write: tier files are written to temporary files, renamed one by one, and the index.json with new SHA-256s is the last file renamed. If the process dies mid-way, clients keep reading the previous dataset. Never partial dataset. If upstream sources fail, the job aborts without publishing: better a one-day-old dataset than an incomplete one.

How to read a report

Aggregated score 0-100. Computed as 100 - sum(severity_weights), with weights: critical 50, high 20, medium 10, low 5, info 0. Reading: 80-100 green (reasonable posture), 50-79 yellow (interventions plannable), 0-49 red (significant risk, some vectors require immediate mitigation). The score does not measure overall backend quality, only the single token's posture against known OWASP/CVE patterns.

Finding severity. critical: confirmed vector enabling forgery or signature bypass directly (alg=none accepted, alg confusion verified, HS256 secret found in dataset). Immediate mitigation. high: probable but unconfirmed vector from manifest alone (jku/x5u without backend pinning verifiability, weak secret in untested tier, lifetime too long, exp missing). Investigate and mitigate in current sprint. medium: suboptimal practice that reduces defense in depth (aud/iss missing, kid suspicious, exp far future). Hardening backlog. low: missed best practice (iat/jti missing). Cleanup when convenient. info: observational state (typ missing, exp expired, cty/zip present). Reporting only.

What to do, by priority. (1) alg=none / alg permutation CRITICAL: the backend must explicitly reject any alg outside a whitelist (HS256, RS256, ES256). Immediate patch, regression tests with malformed tokens. (2) alg confusion confirmed CRITICAL: the backend must use APIs with algorithm-specific verification (e.g. jwt.verify(token, key, { algorithms: ['RS256'] }) in jsonwebtoken), not auto-detect from the header. (3) weak secret HIGH/CRITICAL: rotate the secret immediately to a crypto-safe random string of 32+ bytes (openssl rand -base64 32) and invalidate all tokens in circulation. (4) jku/x5u HIGH: the backend must pin the URL to a whitelist or ignore these headers entirely and use a hardcoded JWKS endpoint only. (5) kid suspicious MEDIUM: validate kid against an alphanumeric whitelist, exact lookup in an internal map, never use it as an SQL query parameter or a file path. (6) missing claims: add exp, aud, iss, jti to the token-emission template. Validate server-side with reasonable leeway (5-30 seconds).

Alg confusion finding format. When the check passes with the provided pubkey, the finding reports: the token, the public key used as secret, the algorithm declared in the header, a textual description of the vector. Hand it to the team with the operational message: "the backend must verify the token with the specifically expected algorithm, not the one declared in the header. Suggested patch: constrain the algorithm in the verify signature, or reject HS256 at the application level if the architecture uses RS256".

Summary table of applied checks

CheckSeverityReference
alg=none / alg=None / alg=NONE / alg=nullCriticalCVE-2015-9235 (jsonwebtoken < 8.5), CVE-2018-7651 (jose < 4)
alg permutation (case-mix, empty, numeric)CriticalOWASP JWT Cheatsheet
alg confusion confirmed (HS256 with RSA pubkey as secret)CriticalCVE-2016-10555, OWASP JWT Cheatsheet
HS256 with secret in fast tier (top 1,000)CriticalOWASP JWT Cheatsheet
HS256 with secret in full tier (1,001-10,000)HighOWASP JWT Cheatsheet
exp missingHighRFC 7519 sec. 4.1.4
exp already expiredInfoRFC 7519
exp more than 1 year in the futureMediumOWASP JWT Cheatsheet
iat missingLowRFC 7519 sec. 4.1.6
aud missingMediumOWASP confused deputy
iss missingMediumOWASP rogue IdP
jti missingLowRFC 7519 sec. 4.1.7
Lifetime > 30 daysHighOWASP JWT Cheatsheet
kid present with suspicious chars (path/SQLi)MediumOWASP Injection Cheatsheet
jku presentHighOWASP JWT spec abuse
x5u presentHighOWASP JWT spec abuse
cty present (nested JWT)InfoRFC 7519 sec. 5.2
zip present (DEFLATE)InfoRFC 7516 sec. 4.1.3
typ missingInfoRFC 7519 sec. 5.1

Glossary

Technical terms used on this page, briefly explained.

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

Does your API's JWT authentication hold up under a serious audit?

This tool reports risks visible from a single token: alg=none, weak secret, alg confusion, missing claims, esoteric headers. A professional review examines the full flow: OIDC provider configuration, key rotation (manual or automatic), refresh token rotation with anti-replay via jti, server-side logout, audience binding, kid and jku/x5u handling in production, exp leeway, verify-library hardening, exploitability assessment of OWASP vectors against actually-reachable codepaths. If the triage above showed a critical or high finding, the problem already exists: the next step is understanding real impact, costs and timelines of remediation. I work on PHP, Laravel, Symfony, Node.js, Python backends with focus on production APIs and regulatory adaptation.

JWT architecture review

Want a realistic estimate for your project?

7-question wizard, 2 minutes, free. Output: range of person-days, rough price range, engagement recommendation. Reference rate 300 EUR/day. Built for custom backend projects, integrations, security audits or AI automation.

Get a free quote