skip to content

How does RS256-to-HS256 algorithm confusion let an attacker forge a valid JWT?

level: seniorimportance: must knowfreq 62%

answer

  1. Two key models, one verification call
  2. The verifier's key was never secret
  3. Public bytes reused as a MAC secret
  4. Keys should carry their own type
  5. One issuer, one algorithm family

basics

~20 s

The attacker rewrites the header to HS256 and signs the forged token using the issuer's public key bytes as the HMAC secret. A verifier that trusts the header's algorithm hands those same public bytes to the HMAC check, which then succeeds.

solid answer

~50 s

RS256 verification takes a public key; HS256 verification takes a shared secret. The confusion arises when a verifier is written as "take the configured key material and the algorithm named in the header, then verify". The attacker changes the header to `"alg": "HS256"`, edits the claims, and computes an HMAC over the token using the issuer's **public** key — which is not secret at all, published in the issuer's key set — as the MAC key. The verifier reads HS256, treats its stored public key bytes as an HMAC secret, recomputes the same MAC, and matches. Forgery complete, with no private key involved. The defence is that a key must be bound to exactly one algorithm family: pin the expected algorithm in verifier configuration, and use typed key objects so an RSA public key cannot be passed as an HMAC secret at all.

code

json · 4 lines
json
{
  "alg": "HS256",
  "kid": "2024-11-a"
}

go deeper

for a junior

Know that symmetric algorithms use one shared secret while asymmetric ones split private signing from public verification, and that mixing the two on the verifier is dangerous.

for a middle

Explain the substitution concretely: header rewritten to HS256, signature computed as an HMAC over the token using the published public key bytes, verifier feeding those same bytes to the MAC check.

for a senior

Show the defence in depth — algorithm pinned in configuration, keys typed and bound to a family, no cross-family accept lists — plus the negative test that proves it stays closed after upgrades.

for a principal

Own the invariant platform-wide: a credential never influences how it is verified. Enforce it through a shared verification component rather than trusting each service to configure its own accept list.

## The setup JWT signature algorithms fall into two families with fundamentally different key models. Symmetric algorithms such as HS256 use one shared secret for both producing and checking the MAC — anyone who can verify can also forge. Asymmetric algorithms such as RS256 use a private key to sign and a public key to verify — the verifier holds material that is deliberately published and cannot mint tokens with it. Algorithm confusion collapses that distinction by making the verifier use asymmetric key material in a symmetric routine. ## The attack, step by step 1. The system issues RS256 tokens. Verifiers hold the issuer's RSA public key, which is public by design — served from the issuer's key set, often retrievable in a single request. 2. The attacker takes a token, edits the payload to elevate privileges, and rewrites the header to `{"alg":"HS256","kid":"..."}`. 3. The attacker computes `HMAC-SHA256(key = exact bytes of the issuer's public key, message = base64url(header) + "." + base64url(payload))` and uses that as the signature. 4. The vulnerable verifier reads `alg` as HS256, fetches "the key for this issuer" — the RSA public key it has cached — and passes those bytes to the HMAC verification routine. 5. Both sides computed the same MAC with the same key over the same bytes, so verification succeeds. The attacker never touched the private key. The only secret required was something published on purpose. ## The exact byte representation matters to the attacker In practice the attacker must match the byte encoding of the public key that the verifier passes in — a PEM-encoded SPKI block including headers and newlines, a DER blob, or a modulus reconstructed from the key set's `n` and `e` values. That is a small amount of trial and error over a handful of common encodings, not a barrier. Treat the requirement as a formality rather than a mitigation. ## Root cause The vulnerability is not in RSA and not in HMAC. It is an API and configuration flaw: **the verifier let the token select the algorithm while holding a single untyped blob of key material**. Two design errors compound: - The algorithm is read from attacker-controlled input rather than from configuration. - The key is a byte string with no attached notion of "this is an RSA public key, usable only for RSA verification". Either error alone is survivable; together they are a full authentication bypass. ## Defences, strongest first **1. Pin the algorithm at the verifier.** State "this issuer signs RS256" in configuration and reject any token whose `alg` differs, before key lookup. This alone closes the attack. **2. Bind keys to algorithms.** A key set entry carries `kty` (key type), and may carry `alg` and `use`. Select only keys whose type and declared algorithm match the operation you are performing, so an RSA entry can never satisfy a MAC verification. Libraries that model keys as typed objects rather than byte arrays make the mistake unrepresentable — passing an RSA public key where an HMAC secret is required simply will not compile or will throw. **3. Do not mix families for one issuer.** A verifier that accepts both HS256 and RS256 from the same issuer has to be exactly right about which key goes with which algorithm on every path. Do not accept an algorithm set spanning families. **4. Separate key material by purpose.** Never store an HMAC secret and a public key in the same configuration slot, and never let one lookup function return either. ## Detection and testing Write a negative test that performs the attack: take a legitimate RS256 token, rewrite the header to HS256, edit a claim, and sign with the issuer's public key bytes in the encodings you actually publish. Assert an authentication failure. This test is worth keeping permanently — it protects against a library upgrade that loosens defaults or a configuration change that widens the accepted algorithm list. Operationally, alert on any token arriving with an unexpected `alg` for a known issuer. Legitimate clients never produce one, so a non-zero rate means either a misconfigured service or someone probing. ## The wider family The same shape appears in other substitutions — an unsecured token with an empty signature, or a header field that redirects key lookup to a source the attacker controls. All of them are instances of one rule: **the credential must never influence how the credential is checked**. Algorithm, key, and key source are the verifier's decisions, derived from configuration. A candidate who states that rule and then derives the specific attacks from it is demonstrating the understanding the question is testing.

  • Why is the issuer's public key enough to forge here, when it cannot produce an RSA signature?
    Because HMAC does not care what the bytes mean — any byte string works as a MAC key. The attacker is not producing an RSA signature at all; they are producing a MAC whose key happens to be the published public key. Verification succeeds because the vulnerable verifier feeds those same bytes into the same MAC routine.
  • Does requiring a kid header stop this attack?
    No. The attacker keeps the original `kid` so the verifier resolves the same public key — that is exactly what the attack needs. `kid` selects which key, while the vulnerability is about which *algorithm* is applied to it. Only pinning the accepted algorithm, or binding each key to its algorithm family, closes it.
  • How do typed key objects make this class of bug unrepresentable?
    When a library models keys as typed values — an RSA public key type distinct from a MAC secret type — a MAC verification function simply cannot accept an RSA public key; the mistake fails at compile time or throws immediately. Untyped byte arrays let the same value flow into either routine, which is what turns a configuration slip into a bypass.
  • Is it ever acceptable to accept both HS256 and RS256 from one issuer?
    Avoid it. Supporting both means every code path must pair the right key with the right algorithm without exception, and one lookup that returns the wrong material recreates the attack. If you must migrate between families, run distinct issuers or distinct configuration entries so the key sets never overlap, and retire the old family quickly.

saying these in an interview costs you the question

  • Believes only the private-key holder can create any valid token
  • Thinks a public key is unusable as an HMAC secret
  • Stops the attack by requiring a kid header
  • Accepts multiple algorithm families for one issuer
  • Blames RSA or HMAC rather than the verifier's key handling

context