Dns Cache Poisoning with Jwt Tokens
How DNS Cache Poisoning Manifests in JWT Tokens
DNS cache poisoning can subvert the trust chain that many JWT‑based services rely on when they fetch signing keys or validate issuer metadata. The most common vector is the jku (JWK Set URL) header defined in RFC 7515. When a service accepts a JWT that contains a jku header, it will dereference that URL to obtain the public key used for signature verification. If an attacker can poison the DNS resolver used by the service, they can redirect a legitimate‑looking jku value (e.g., https://keys.example.com/.well-known/jwks.json) to an attacker‑controlled server that returns a malicious key pair. The service then verifies the token with the attacker’s private key, accepting a forged token as valid.
Another pathway involves the iss> (issuer) claim. Some libraries resolve the issuer URL to discover a JWKS endpoint (e.g., OpenID Connect discovery). If the DNS response for the issuer domain is poisoned, the library may fetch keys from a malicious source, again enabling token forgery. Both scenarios require the target service to perform unauthenticated outbound HTTP requests to domains that are not hard‑coded or pinned, making the DNS cache a critical attack surface.
Real‑world analogues include CVE‑2020‑13757 (Keycloak) where an improperly trusted jku header allowed token bypass, and various OAuth/JWT implementations that blindly follow issuer‑provided JWKS URLs without validating the TLS certificate or checking against an allow‑list.
JWT Tokens‑Specific Detection
Detecting this issue starts with identifying whether a service accepts or acts on untrusted jku headers or resolves issuer URLs without strict validation. middleBrick’s unauthenticated black‑box scan probes the API endpoint for typical JWT validation behaviours:
- It sends a JWT containing a
jkuheader that points to a non‑routable test domain and monitors whether the service attempts to fetch that URL (via outbound DNS/HTTP checks performed safely in the scan). - It checks for missing issuer validation by presenting a token with a fabricated
issclaim that resolves to a test domain and observes whether the service treats the token as valid. - It enumerates the API’s OpenAPI/Swagger spec (if present) to locate operations that consume JWTs and examines associated security schemas for missing
x-jku-disabledor similar extensions that would indicate a deliberate lock‑down.
If the service processes the token without rejecting the jku header or without validating the issuer against a strict allow‑list, middleBrick flags the finding under the "Authentication" category with a severity of "high". The report includes the exact request that triggered the behaviour, the response code, and guidance on where in the code path the validation occurs.
Because the scan is agentless and requires only the public URL, teams can run it continuously via the middleBrick CLI (middlebrick scan https://api.example.com) or embed it in a CI pipeline with the GitHub Action to catch regressions before deployment.
JWT Tokens‑Specific Remediation
The most reliable defence is to eliminate reliance on dynamic key retrieval unless absolutely necessary, and to harden any remaining dynamic look‑ups.
1. Disable acceptance of the jku header entirely. Most JWT libraries allow you to reject tokens that contain this header. In Node.js using the jsonwebtoken package, you can verify the header before decoding:
const jwt = require('jsonwebtoken');
function verifyToken(token) {
const decoded = jwt.decode(token, { complete: true });
if (decoded.header && decoded.header.jku) {
throw new Error('JWK Set URL (jku) header is not allowed');
}
// Verify with a known static key or trusted JWKS endpoint
return jwt.verify(token, process.env.PUBLIC_KEY, { algorithms: ['RS256'] });
}
2. If a JWKS endpoint must be used (e.g., for third‑party IdPs), enforce TLS certificate pinning or restrict the domain to an explicit allow‑list. Using the jose library, you can create a fixed RemoteJWKSet that only accepts keys from a hard‑coded URL:
import { jwtVerify } from 'jose';
const jwksUrl = 'https://auth.example.com/.well-known/jwks.json';
const { key } = await jwtVerify(token, await import('jose').createRemoteJWKSet(new URL(jwksUrl)));
// Additional checks
const payload = jwtDecode(token);
if (payload.iss !== 'https://auth.example.com/') {
throw new Error('Invalid issuer');
}
3. Validate the iss and aud claims against a strict list of trusted values. Never allow the issuer field to dictate where keys are fetched.
4. Deploy network‑level controls: ensure outbound DNS queries from the application host go only to trusted resolvers, and consider using DNSSEC or encrypted DNS (DoH/DoT) to reduce poisoning risk.
By combining code‑level hardening (rejecting jku, strict claim validation) with operational controls (pinned TLS, DNS hardening), you eliminate the attack surface that DNS cache poisoning exploits in JWT validation flows.