
JWT JSON Web Token was published as RFC 7519 in 2015. The alg: none vulnerability was documented that same year. The attack is eleven years old. The CVEs that trace to JWT implementation failures caused by alg: none, by algorithm confusion, by weak signing secrets, and by missing validation checks have accumulated in public databases since 2015.
And yet.
In 2026, penetration tests against web applications using JWT-based authentication continue to find alg: none acceptance, algorithm confusion vulnerabilities, weakly signed tokens, and missing claim validation not occasionally, not in legacy systems that nobody has touched since 2017, but in applications that were built recently, using modern frameworks, by developers who were aware that JWT security was something they needed to think about.
The gap between awareness and implementation is the story. Most developers who implement JWTs know the concept of token signing. Fewer understand the specific library configurations that produce vulnerable behavior in practice. Fewer still understand that the vulnerability is not always in the code they wrote it is in the configuration they did not set, the library default they did not override, or the validation step they assumed was handled somewhere upstream.
This post is about where the assumptions break down, why they keep breaking down, and what correct JWT implementation actually requires in 2026.
What JWT Is and What Makes It Trustworthy
A JSON Web Token is a compact, URL-safe means of representing claims between two parties. It consists of three base64url-encoded segments separated by periods: a header, a payload, and a signature.
The header specifies the token type and the signing algorithm. The payload contains the claims the assertions about the user, the session, the token's validity window, and any application-specific data the token carries. The signature is produced by running the encoded header and payload through the signing algorithm using a key known to the issuer.
The trustworthiness of a JWT derives entirely from the signature. If the signature is valid if it was produced by someone who holds the correct signing key the claims in the payload can be trusted. If the signature is not valid, the claims in the payload should not be trusted, regardless of what they say.
This means that the critical step in JWT processing is signature verification. Every other JWT security property claim validation, expiration enforcement, audience restriction depends on the signature verification being correct. A JWT with an invalid signature but a non-expired, correctly-scoped payload should be rejected. A JWT with a valid signature but an expired exp claim should also be rejected. Both validations are required. The signature check establishes that the token is authentic. The claim validation establishes that it is currently valid.

The alg: none Attack: Why It Still Works
The alg: none attack is this: an attacker takes a valid JWT, modifies the payload to contain elevated claims, changes the alg field in the header to none, removes the signature, and submits the modified token. A vulnerable library accepts the token because it sees alg: none and concludes that no signature verification is needed.
The RFC permits the none algorithm. It was included to allow for cases where the token's integrity is established by other means channel-level security, for instance. The RFC also specifies that implementations must explicitly allow the none algorithm if it is to be accepted. Many JWT libraries, in their default configurations, accept none without requiring explicit enablement. The developer who uses the library without reading the security-relevant configuration options and most developers do not read the full configuration documentation for every library they use ships a vulnerability.
Here is what the attack looks like in practice.
Step 1: Obtain a valid token. Register or authenticate as a low-privilege user. The application issues a JWT. Decode it using any base64url decoder:
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"sub": "user456", "role": "member", "exp": 1719435600}
Step 2: Modify the payload. Change the claims to reflect desired privileges:
Payload (modified): {"sub": "user1", "role": "administrator", "exp": 9999999999}
Step 3: Change the algorithm to none and remove the signature:
Header (modified): {"alg": "none", "typ": "JWT"}
Signature: (empty)
Step 4: Reconstruct the token. The result is:
base64url(modified_header).base64url(modified_payload).
Note the trailing period the empty signature segment. Some libraries require the period to be present even when the signature is empty. Others accept the token with or without it.
Step 5: Submit the token. Replace the original JWT in the Authorization header with the modified token. If the server accepts it, the attack succeeded.

Why Libraries Keep Getting This Wrong
The alg: none vulnerability has been patched in every major JWT library. CVEs have been issued. Blog posts have been written. Conference presentations have been given. The question worth spending time on is why the vulnerability keeps appearing in production applications despite all of that.
The answer is not developer ignorance. It is library complexity interacting with developer time constraints.
Most JWT libraries have three or four distinct APIs for token verification, each with different security properties depending on how they are called. A library might expose:
verify(token, secret)- verifies the signature using the provided secret, but may still acceptnoneas a valid algorithmverify(token, secret, {algorithms: ['HS256']})- verifies the signature and restricts acceptable algorithms to the specified list, rejectingnonedecode(token)-decodes the token without any verification whatsoever, intended for reading claims after verificationdecode(token, {complete: true})-decodes all segments including the header, again without verification
A developer who reaches for verify() and passes the secret has done something reasonable. They have used the verification API rather than the decoding API. But if they have not explicitly restricted the algorithms parameter, the library may still accept a token that specifies none as its algorithm, because the library's verify() call honors the algorithm specified in the token header rather than enforcing the algorithm the developer intended.
The correct call, in most JavaScript JWT libraries, is:
javascript
jwt.verify(token, secret, { algorithms: ['HS256'] });
Not:
javascript
jwt.verify(token, secret);
The second call looks correct. It uses the verification API. It passes the secret. But the absence of the algorithms option means the library will use the algorithm specified in the token header which the attacker controls.

This is not the developer's fault in any simple sense. The library's API does not make it obvious that verify() without algorithms is different from verify() with algorithms. The documentation may mention it, but the mention is in a security considerations section that most developers reach only after they have already shipped the first implementation.
Algorithm Confusion: The RS256 to HS256 Switch
Algorithm confusion is a related but distinct attack. Where alg: none exploits a library's willingness to skip verification entirely, algorithm confusion exploits a library's confusion about which key to use for verification when the algorithm in the token header is changed.
The setup: an application uses asymmetric signing. The server generates an RSA key pair, signs tokens with the private key using RS256, and publishes the public key at a JWKS endpoint. Token verification checks the signature against the public key.
The attack: the attacker obtains the server's RSA public key which is publicly available at the JWKS endpoint, by design and uses it as an HMAC secret to sign a modified token. The modified token's header specifies HS256 instead of RS256. When the library receives this token, it sees HS256 in the header, looks for an HMAC secret to verify against, and uses whatever key material it has configured which, in a confused library, may be the RSA public key.
Because the attacker signed the token using the RSA public key as the HMAC secret, and the library verifies it using the same RSA public key, the verification passes. The attacker has produced a valid-seeming signature for a token they constructed, using only publicly available key material.

The fix is the same as for alg: none: explicitly restrict which algorithms the library will accept and never use an algorithm specified by the token header as the authority on which key to use.
javascript
// RS256 specifically required algorithm confusion prevented
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
When the library is explicitly told to expect RS256, a token claiming HS256 is rejected regardless of whether its signature verifies.
Weak Signing Secrets: Brute Force in Practice
The alg: none and algorithm confusion attacks work when signature verification is either absent or confused. A different class of JWT vulnerability applies when signature verification is correct but the secret used for signing is weak.
HMAC-SHA256 signatures are only as strong as the secret they use. A JWT signed with secret, password, mysecret, or any other guessable string can be brute-forced using publicly available wordlists in seconds. Tools designed for this purpose hashcat with JWT-specific modes, jwt-cracker, and similar utilities take a captured JWT and attempt to find the signing secret by trying candidate values until one produces a signature that matches.
The captured JWT is available to any authenticated user it is their own token. The only protection against brute force is the quality of the signing secret.
What constitutes a weak signing secret in practice: any string that appears in password wordlists, any string under 32 characters, any string that is based on guessable application-specific information (the application name, the domain, a default value from a tutorial or documentation example), and any secret that appears in environment variable names in public documentation (JWT_SECRET=yoursecretkey in a README is a weak secret that is now part of every brute force wordlist).
The correct practice is a cryptographically random secret of at least 256 bits (32 bytes), generated using a secure random number generator, stored in a secrets manager, and rotated periodically. Not mysecret. Not your-256-bit-secret-here. Not the value that appears in the JWT library's documentation example and has therefore been tried by every automated cracking tool that exists.

Missing Claim Validation: The Correct Signature On The Wrong Token
Even when signature verification is correctly implemented and the signing secret is strong, JWT vulnerabilities can arise from incomplete claim validation.
The JWT specification defines several registered claims that carry security-relevant meaning:
exp (expiration time): The token should be rejected after this timestamp. If exp is not validated, a token captured today is valid indefinitely a captured token becomes a permanent credential.
nbf (not before): The token should not be accepted before this timestamp. Rarely validated because the failure mode is subtle, but pre-issuance token acceptance can be exploited in specific timing scenarios.
iss (issuer): The token should be rejected if the issuer does not match the expected issuer. Without issuer validation, a token issued by a different system including an attacker-controlled system is accepted if it has a valid signature structure.
aud (audience): The token should be rejected if the audience does not include the expected value. Without audience validation, a token intended for Service A can be replayed against Service B.
sub (subject): The subject identifier should correspond to a real user in the application's user database. Without subject validation, a forged or replayed token with an arbitrary sub value may successfully authenticate as a user who does not exist or who has been deactivated.
The most common omission in practice is iss and aud validation. Developers who implement JWT authentication in a single-service application often think of the iss and aud fields as metadata rather than security controls. When the application later expands to multiple services, or when an attacker presents a token from a different issuer, the missing validation becomes a cross-service token reuse or token forgery vulnerability.

The JWK Injection Attack
A more recent JWT vulnerability class that has gained prominence: JWK injection, also known as the embedded JWK attack.
The JWT header can optionally include a jwk parameter containing the public key that should be used to verify the token. The purpose is to allow key distribution in certain protocol contexts. The vulnerability: some JWT libraries, when they encounter a jwk parameter in the token header, use the embedded key to verify the token rather than the server's configured key.
An attacker who can embed their own public key in the token header, and who signs the token with the corresponding private key, produces a token that a vulnerable library will verify using the attacker's embedded key. The signature verification passes the token is correctly signed by the private key corresponding to the embedded public key. The library has accepted a token it should not accept, because the key used for verification came from the attacker, not from the server's configuration.
The fix is explicit: the JWT library must be configured to ignore the jwk header parameter and use only the server's configured key for verification. Any key material embedded in the token header by the attacker should be disregarded.

Testing JWT Implementations: A Structured Approach
Testing a JWT implementation requires exercising each vulnerability class systematically. The following sequence covers the most impactful attack vectors.
Step 1: Capture and analyze a valid token. Authenticate as a low-privilege user. Decode the JWT using any base64url decoder the header and payload are not encrypted, only encoded. Note the algorithm, the claims structure, the issuer, the audience, the expiration window, and any application-specific claims.
Step 2: Test alg: none. Modify the header to set alg: none, modify the payload to include elevated claims, remove the signature, and submit. Test with and without the trailing period after the second separator. Test with the algorithm as None, NONE, and none some libraries match case-sensitively.
Step 3: Test algorithm confusion. If the original token uses RS256, obtain the public key from the JWKS endpoint. Sign a modified token with the public key as HMAC-SHA256 secret, set the header to alg: HS256, and submit. If the original token uses HS256, attempt to brute-force the signing secret.
Step 4: Test claim validation. Submit a token with exp set to a past timestamp. Submit a token with iss changed to an arbitrary value. Submit a token with aud removed or changed. Submit the same token twice within its validity window to test for JTI / nonce validation.
Step 5: Test JWK injection. Generate a key pair. Embed the public key in the JWT header as a jwk parameter. Sign the modified token with the corresponding private key. Submit.
Step 6: Test token scope. If the application issues different token types access tokens, refresh tokens, ID tokens test whether each token type is accepted in contexts intended for the others. A refresh token submitted as an access token should be rejected.

What Correct Implementation Looks Like
The complete JWT security checklist for a production application is not long, but each item is non-negotiable:
Library configuration: Explicitly specify the accepted algorithm. Never trust the algorithm from the token header. Disable none explicitly if the library does not disable it by default. Disable JWK header parameter processing if the library supports it.
Secret quality: Use a cryptographically random secret of at least 256 bits for HMAC algorithms. For asymmetric algorithms, use a key length appropriate for the algorithm (2048 bits minimum for RSA). Store secrets in a secrets manager. Do not use tutorial values, application names, or predictable strings.
Claim validation: Validate exp on every token. Validate iss against the expected issuer. Validate aud against the expected audience. Validate sub against the user database.
JTI / nonce: For high-value token types, implement JTI tracking to prevent replay within the token's validity window.
Token lifetime: Keep access token lifetimes short. Thirty minutes to one hour is a reasonable default for most applications. Long-lived access tokens extend the window during which a captured token remains useful.
Error handling: JWT validation failures should return a 401 with a generic error message. Do not include the specific reason for rejection — whether it was an invalid signature, an expired token, or a wrong issuer — in the response. Verbose JWT rejection messages help an attacker understand which component of their forged token needs to be corrected.
Why 2026 Is Not Too Late to Find These
The alg: none attack is eleven years old. The question of why it still appears in production applications deserves a direct answer.
New applications are built every day using JWT authentication, and the decisions made in the first implementation of an authentication system tend to persist. An application that was built in 2022 with a JWT library configured incorrectly is still running in 2026 with the same configuration. The authentication middleware was written once, tested functionally (login works, logout works, session expires), and not revisited.
New libraries emerge with different API designs and different default behaviors. A developer who has worked with one JWT library and understands its security-relevant configurations migrates to a different library and encounters a different API design where the security-relevant options are in different locations or have different names. The old mental model does not fully transfer.
Framework updates occasionally change the default behavior of JWT handling — sometimes improving security, sometimes introducing new configuration requirements that are not surfaced prominently in migration guides.
The vulnerability is not aging out because the conditions that produce it complex APIs with non-obvious security defaults, time-constrained implementation, configuration that looks correct but is not are stable properties of how software development works. They do not go away as the vulnerability class becomes older and better-documented.
Closing: The Signature Was The Whole Point
JWT was designed to allow one party to make verifiable claims to another. The verification is cryptographic. The security is in the signature.
Every attack in this post exploits the same gap: a system that was supposed to verify the signature did not, either because a library accepted tokens with no signature, or because the library was confused about which key to use, or because the secret was recoverable, or because the signature was checked but the claims that determined the scope of trust were not.
The token did not lie. The system did not ask the right questions about it.
Correct JWT implementation is not complicated. It requires explicit algorithm restriction, strong secret generation, complete claim validation, and awareness of the specific library configurations that produce vulnerable behavior. None of these is difficult engineering. All of them require deliberate attention rather than the default assumptions that produce insecure implementations.
The alg: none attack works in 2026 because not every implementation has given it that deliberate attention. The question for any specific application is which category it is in and the answer requires testing the token validation logic with the specific intent to find the gaps, not with the assumption that because JWT is being used, the authentication is secure.
Axeploit tests JWT implementations as part of its authentication coverage probing alg:none acceptance with multiple case variants, testing algorithm confusion against RS256-signed applications using the public JWKS key, attempting brute force against common signing secret patterns, validating claim enforcement for exp, iss, aud, and sub, and testing JWK injection where applicable. Its "No Exploit, No Report" policy means JWT findings are confirmed authentication bypasses, not theoretical flags. The testing runs against the deployed application, on the actual authentication middleware, with the actual library configuration that shipped to production.





