Why a JWT verifier shouldn't trust alg: none, RS256→HS256 confusion and algorithm pinning
A JWT's alg header is chosen by the token's sender. Why alg none and RS256→HS256 confusion work, what RFC 8725 requires (one algorithm per key), and the iss, aud, exp, typ checks that follow.
The first part of a JWT, the header, carries alg, which says which algorithm signed the token. The catch is who writes that value: the sender of the token. If the verifier picks its verification method by reading alg, the attacker is the one picking it.
This post follows RFC 8725, the JWT best-practices document (BCP 225, February 2020, which updates RFC 7519), and the specifications it builds on: RFC 7515 (JWS), RFC 7518 (JWA) and RFC 7519 (JWT). It covers JWTs represented using JWS, including Unsecured JWTs (alg: none); encrypted JWTs (JWE) are out of scope. As of 10 October 2026, the RFC Editor’s info page for RFC 8725 lists no RFC that obsoletes or updates it.
alg lives inside the token
RFC 7515 Section 4.1.1 defines alg as the header parameter naming the cryptographic algorithm used to secure the JWS, and it MUST be present. The registered values are in the table in RFC 7518 Section 3.1: HMAC as HS256, HS384 and HS512; RSA signatures in the RS256 and PS256 families; ECDSA in the ES256 family; and none, meaning no signature at all.
The header is just Base64URL-encoded JSON, so anyone can read it and rewrite it (see the Base64 post). In the JWS Compact Serialization (the usual form, split by two dots) the header is part of the signing input, so changing it breaks the original signature. RFC 7515 Section 10.7 also lists carrying alg in the protected header as a mitigation against algorithm substitution.
That protection only means something when the verifier checks the signature the way it decided to. Changing alg does not by itself let an attacker produce a valid signature under any algorithm they like. The problem arises when the verifier accepts an attacker-selected method that the attacker can also satisfy, such as none, or HS256 with a known RSA public key used as the HMAC secret. Then the attacker can replace the header and create a value that the flawed verifier accepts.
Two known failures
RFC 8725 Section 2.1 describes two cases where the alg header, combined with design flaws in libraries and applications, led to attacks.
The first is changing alg to none. Some libraries trusted that value and “validated” the JWT without checking any signature. The unsecured JWT example in RFC 7519 Section 6.1 looks like this (line breaks for display only):
eyJhbGciOiJub25lIn0
.
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt
cGxlLmNvbS9pc19yb290Ijp0cnVlfQ
.
The first part is {"alg":"none"}, the middle is {"iss":"joe","exp":1300819380,"http://example.com/is_root":true} (the original contains line breaks), and the signature after the last dot is the empty string. No key is involved, so anyone can produce one.
The second is changing RS256 (an RSA signature) to HS256 (HMAC). Some libraries then tried to validate an HMAC-SHA256 using the RSA public key as the HMAC shared secret (CVE-2015-9235, cited by RFC 8725). A public key is, as the name says, public, so the attacker can compute an HMAC over the same bytes and produce a signature that passes. This happens when the verification call says only “verify with this key” and leaves the algorithm to the header.
What the specifications require
RFC 7515 Section 5.2 ends the JWS validation procedure by saying that which algorithms may be used is an application decision, and that even if a JWS validates, the application SHOULD consider it invalid unless the algorithm is acceptable to it. RFC 7519 Section 7.2 says the same for JWTs.
RFC 8725 puts it more strongly:
- Libraries MUST let the caller specify a supported set of algorithms and MUST NOT use any others (Section 3.1).
- The library MUST make sure that
algnames the same algorithm that the cryptographic operation actually uses (Section 3.1). - Each key MUST be used with exactly one algorithm, and this MUST be checked when the operation is performed (Section 3.1).
- Applications MUST only allow cryptographically current algorithms that meet their security requirements (Section 3.2).
The rules for none are in RFC 7518 Section 3.6. Implementations MUST NOT accept unsecured JWSs by default, and to mitigate downgrade attacks applications MUST NOT signal acceptance of none at a global level; they SHOULD signal it per object. Section 8.5 gives the reason. Suppose an application requires a signature on objects received over HTTP but accepts none over HTTPS with client authentication. If it turns none on globally, an attacker can send an unsecured JWS over HTTP and it will still validate.
One more detail. RFC 7519 Section 8 says conforming JWT implementations MUST implement HS256 and none. You can’t count on a library simply not knowing about none; the place to stop it is the verification call. RFC 8725 Section 3.2 likewise says libraries SHOULD NOT consume none JWTs unless the caller explicitly asks.
For completeness, RFC 8725 Section 3.2 also says none can be acceptable when the JWT is protected end to end by a transport layer such as TLS, with the condition that it is used only when the JWT is cryptographically protected by other means.
Bind the algorithm to the key
A simple way to meet these requirements is to keep the algorithm in the key configuration, not in the token. The following is illustrative pseudocode showing the structure, not the API of any particular library.
// Illustrative pseudocode, not the API of any particular library.
// kid → { the one algorithm allowed for this key, the key }
const trustedKeys = new Map([
["2026-10-a", { alg: "ES256", key: issuerPublicKeyA }],
]);
function verifyJwt(token) {
const header = decodeProtectedHeader(token); // nothing trusted yet
const entry = trustedKeys.get(header.kid); // look up only in the trusted set
if (!entry) return reject("unknown key");
if (header.alg !== entry.alg) return reject("algorithm mismatch");
if (!verifySignature(entry.alg, entry.key, token)) return reject("bad signature");
// only after the signature checks out: iss, aud, exp, nbf, typ
}
Whether alg says none or HS256, if it differs from the value recorded for the key, the code never reaches the signature computation. The algorithm used for verification also comes from entry.alg, not from the header. If the issuer uses a single key, one fixed key and one algorithm are enough, with no kid needed.
The headers used to select a key are untrusted input too:
kidis a hint about which key was used, and its structure is unspecified (RFC 7515 Section 4.1.4). If it feeds a database or LDAP lookup, validate or sanitize it so it can’t cause SQL or LDAP injection (RFC 8725 Section 3.10).- Blindly following
jku(JWK Set URL) orx5u(X.509 URL) means sending requests to arbitrary URLs, which can lead to SSRF. RFC 8725 Section 3.10 suggests matching the URL against an allowlist and not sending cookies on the GET request (SHOULD). - The way public keys are obtained must authenticate the key’s origin (RFC 7515 Section 10.3). A key the token carries for itself in a
jwkheader has no authenticated origin unless something else confirms it belongs to the issuer.
With HMAC, the key itself matters. RFC 7518 Section 3.2 requires a key at least as large as the hash output, 256 bits for HS256 (MUST). RFC 8725 Section 3.5 says human-memorizable passwords MUST NOT be used directly as the key for a MAC such as HS256; per Section 2.2, a weak key like that is open to offline brute-force or dictionary attacks once an attacker holds a single token. The comparison between the computed HMAC and the received signature MUST be constant-time to resist timing attacks (RFC 7518 Section 3.2).
After the signature checks out
A valid signature tells you that someone with this key signed this content, and no more. RFC 7519 Section 11.1 says a JWT’s contents can’t be relied on in a trust decision unless they are cryptographically secured and bound to the context the decision needs.
- iss: when
issis present, the application MUST check that the signing key belongs to that issuer and reject the JWT if not (RFC 8725 Section 3.8). How keys are bound to issuers is application-specific; OpenID Connect fetches them from thejwks_uriin the issuer’s metadata. - sub: when
subis present, the application MUST check that it is a valid subject, or issuer-subject pair, for the application (Section 3.8). - aud: if one issuer issues JWTs for more than one recipient, the JWT MUST contain
aud, and a recipient MUST reject it when the value is missing or not associated with itself (Section 3.9). This is what stops a token issued for one service from being replayed at another (the substitution attack in Section 2.7). - exp / nbf: the JWT MUST NOT be accepted on or after
expor beforenbf(RFC 7519 Sections 4.1.4 and 4.1.5). A small leeway for clock skew, usually no more than a few minutes, MAY be allowed. - typ: if the same issuer issues more than one kind of JWT, the validation rules MUST be mutually exclusive so each rejects the other kinds (Section 3.12). For new kinds of JWT, explicit typing with
typis RECOMMENDED (Section 3.11); Security Event Tokens, for example, usesecevent+jwt.
Validation checklist
- Pin the allowed algorithms in configuration and reject any header
algnot on that list. - Give each key exactly one algorithm and check that
algmatches it. - Reject
noneby default. Allow it only for the specific object that needs it. - Take keys only from trusted configuration or issuer metadata. Sanitize
kid; matchjkuandx5uagainst an allowlist. - Use a randomly generated key of sufficient length for HMAC: at least 256 bits for
HS256, never a password. - After the signature, check that the key belongs to
iss, thensub,aud,expandnbf, andtypwhere it applies. - If any step fails, reject the whole JWT (RFC 7519 Section 7.2, RFC 8725 Section 3.3).
What this does not cover
Pinning the algorithm removes the problem of the token choosing how it gets verified. These remain:
- Replay of a valid token: if a legitimately issued token leaks, its signature still verifies. RFC 7515 Section 10.10 mentions including a unique identifier and checking that it hasn’t been seen before, and
jtican serve that purpose (RFC 7519 Section 4.1.7). - Signing key compromise: if the private key or MAC key leaks, an attacker can sign like the real issuer (RFC 7515 Section 10.2).
- Confidentiality: a signature prevents forgery; it does not hide the payload.
- JWE and nested JWTs: skipping signature validation after decryption (RFC 8725 Section 2.3) and ECDH-ES input validation (Section 2.5) are not covered here.
Which status code to send when validation fails is covered in HTTP 401 vs 403.
References
- RFC 8725 JSON Web Token Best Current Practices (BCP 225): Sections 2.1, 2.2, 2.3, 2.5, 2.7, 3.1, 3.2, 3.3, 3.5, 3.8, 3.9, 3.10, 3.11, 3.12
- RFC 8725 info page: status and updates
- RFC 7515 JSON Web Signature (JWS): Sections 4.1.1, 4.1.4, 5.2, 10.2, 10.3, 10.7, 10.10
- RFC 7518 JSON Web Algorithms (JWA): Sections 3.1, 3.2, 3.6, 8.5
- RFC 7519 JSON Web Token (JWT): Sections 4.1.3, 4.1.4, 4.1.5, 4.1.7, 6.1, 7.2, 8, 11.1
Links and content checked on 10 October 2026.