JWT kid Injection in a Six-Hour Window
An API for a fintech platform issues RS256 JWTs from https://auth.example.com. The token header contains kid, which the auth service uses in a SQLite lookup: SELECT pem FROM keys WHERE kid = '<kid>'. A low-privilege token (role=user, aud=user-api) is valid for 15 minutes and refreshable. A single quote in kid returns HTTP 500 with SQLITE_ERROR and the query fragment; a valid random kid returns 401 with unknown kid. The WAF inspects the request body and query string for SQL keywords but not the Authorization header. alg=none is rejected; HS256 tokens signed with the public key are rejected with alg mismatch. Admin endpoints require role=admin and aud=admin-api. Key rotation happens every 24 hours. You have a 6-hour window and written authorization for read-only access; the client's concern is account takeover.
1. Confirm the kid injection safely and describe the exact steps to turn it into a valid admin token.
Read the worked answer
Confirm safely: send kid=x' and kid=x''. The first returns 500 with a SQLite error; the second returns 401 unknown kid. That quote-termination difference confirms string concatenation without extracting data.
Turn it into a token: the query returns one column (pem). Generate an RSA keypair, then set kid to x' UNION SELECT '<PEM>'--, encoded so newlines survive in the JWT header. The auth service returns your public key for the forged token. Sign the token with your private key, header {"alg":"RS256","kid":"<injection>","typ":"JWT"} and payload {"sub":"admin","role":"admin","aud":"admin-api","exp":<now+900>}. Because the WAF does not inspect Authorization, the payload is not blocked. Cache-busting is irrelevant; the key lookup is per request. SQLite accepts -- comments, and the injected PEM must be a single-quoted SQL literal.
2. What obstacles remain after injection (PEM format, column count, signature, aud/role claims), and how do you handle each?
Read the worked answer
Column count: one, confirmed by the error and by trying UNION SELECT 'x'. PEM format: the service must parse it as a public key; use -----BEGIN PUBLIC KEY----- (SPKI), not a certificate, and preserve newlines. If the server expects a different format, try BEGIN RSA PUBLIC KEY. Header size: a 2048-bit SPKI PEM is about 450 bytes, well under typical header limits.
Signature: the server verifies with the key returned by the query, so your private key must match. aud/role: if authorization reads claims from the token, set them; if it re-reads the DB by sub, you need an existing admin sub, which you may get from an error or a user list. Expiry and jti: set a future exp and use a fresh jti to avoid replay checks. Key rotation does not matter because you are not using the real key set. Finally, confirm the admin endpoint accepts the token before claiming takeover.
3. If the injection works, is this critical? Write the impact statement and the one remediation that matters most.
Read the worked answer
Severity: critical if the forged token is accepted by an admin endpoint, because it is authentication bypass and full admin takeover, not just SQL injection. Impact statement: "An unauthenticated attacker can inject into the kid header, supply their own public key, and forge a token with role=admin and aud=admin-api, resulting in full administrative access to the fintech API." Include the exact request, the returned admin-only response, and the time window.
Primary remediation: parameterize the kid lookup (SELECT pem FROM keys WHERE kid = ?) and, separately, validate kid against the set of active key IDs before any database access. Do not rely on the WAF. Rotating keys does not help because the attack supplies its own key. Validate aud and role server-side as well, but that does not fix the key injection.