Dangling Dns with Jwt Tokens
How Dangling Dns Manifests in Jwt Tokens
JSON Web Tokens (JWT) often carry header parameters that point to remote resources for key material. The most relevant are jku (JWK Set URL), kid when it is a URL, and less commonly x5u (X.509 certificate URL). When a service trusts these URLs without strict validation, an attacker who can control the referenced domain can supply fraudulent keys and sign arbitrary tokens that will be accepted as valid.
A dangling DNS condition occurs when a domain name still resolves (or previously resolved) to a resource that has been de‑provisioned, such as a deleted cloud storage bucket, a removed Heroku dyno, or a terminated Kubernetes service. If the domain is still registered but points to nothing, an attacker can re‑register the same name (or exploit a sub‑domain takeover) and host a malicious JWKS endpoint at the exact URL the application expects.
Typical vulnerable code path looks like this in Node.js using the popular jsonwebtoken library:
const jwt = require('jsonwebtoken');
function verifyToken(token) {
// No algorithm or key restrictions – library will fetch JWKS from jku if present
return jwt.verify(token, undefined, { complete: true });
}
If the token contains a header like { "alg":"RS256", "jku":"https://attacker-controlled.example.com/keys.json"}, the library will download the JWKS from the attacker‑controlled domain, verify the signature with the attacker’s key, and treat the token as legitimate. The same risk exists when kid is a full URL and the library dereferences it to retrieve a public key.
In Python with PyJWT a similar pattern appears when using jwt.decode with a custom key‑fetching function that does not validate the URL:
import jwt
import requests
def get_key(header, payload):
jku = header.get('jku')
if jku:
resp = requests.get(jku) # No URL allow‑list or DNS check
return resp.json()['keys'][0]
# fallback to secret
return SECRET
jwt.decode(token, key=get_key, algorithms=['RS256'])
Both examples illustrate how a dangling DNS‑hosted JWKS can be injected, leading to token forgery, privilege escalation, or unauthorized API access.
Jwt Tokens-Specific Detection
middleBrick detects dangling DNS issues in JWT handling by performing the following steps during its unauthenticated black‑box scan:
- Identifies API endpoints that return JWTs (e.g., login, token refresh, or any endpoint that emits a
Authorization: Bearertoken). - Parses the JWT header to extract
jku,kid(when it looks like a URL), andx5ufields. - For each extracted URL, performs a DNS lookup (A/AAAA and CNAME records). If the domain resolves to an IP address that belongs to a known de‑provisioned cloud service (e.g., an AWS S3 bucket endpoint, Azure blob storage, GitHub Pages, Heroku, Netlify, etc.) or returns no records, the URL is flagged as a potential dangling DNS target.
- Optionally attempts an HTTP GET to the URL; if the response contains a valid JWKS structure, the finding is upgraded to confirmed.
Because middleBrick runs twelve security checks in parallel, this DNS‑dangling check is executed alongside authentication, BOLA/IDOR, input validation, and the LLM/AI security probes, giving you a comprehensive view of the token‑related attack surface.
Example of invoking the scan from the CLI:
npx middlebrick scan https://api.example.com/auth/login
The output includes a property_authorization section (where JWT validation falls) with a finding such as:
{
"id": "JWT-DANGLED-JKU",
"name": "Dangling DNS in JWK Set URL",
"description": "The token header contains jku=https://old‑bucket.example.com/keys.json which resolves to a de‑provisioned S3 bucket. An attacker can re‑register the domain and host malicious keys.",
"severity": "high",
"remediation": "Validate jku/kid URLs against an allow‑list of trusted domains or disable remote JWKS fetching if not required."
}
In the Web Dashboard the same finding appears under the "Authentication" category, allowing you to track the risk score over time and set alerts via email, Slack, or Teams (available on the Pro and Enterprise tiers).
Jwt Tokens-Specific Remediation
The most effective remediation is to eliminate reliance on remote key material unless absolutely necessary. If remote JWKS URLs must be used, enforce strict validation before trusting any key retrieved from those URLs.
1. Disable remote JWKS/kid URLs when not needed
If your application only uses a fixed set of signing keys (e.g., a self‑issued RS256 key), configure the JWT library to ignore jku and kid URL parameters.
Node.js (jsonwebtoken) example:
const jwt = require('jsonwebtoken');
const fs = require('fs');
const PUBLIC_KEY = fs.readFileSync('/etc/keys/public.pem');
function verifyToken(token) {
// No options that allow jku/kid URL fetching
return jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] });
}
Python (PyJWT) example:
import jwt
PUBLIC_KEY = open('/etc/keys/public.pem').read()
def verify_token(token):
return jwt.decode(token, PUBLIC_KEY, algorithms=['RS256'])
2. Implement an allow‑list of trusted domains
When remote key fetching is required (e.g., integrating with an external IdP that publishes JWKS), restrict the URLs to known, controlled domains and verify that each domain resolves to an IP address you own.
Node.js example using jwks-rsa with a custom allow‑list:
const jwksRsa = require('jwks-rsa');
const client = jwksRsa({
jwksUri: 'https://login.example.com/.well-known/jwks.json',
// restrict to this host only
timeout: 3000
});
function getKey(header, callback){
if (header.jku) {
// Reject any jku that is not the trusted IdP
return callback(new Error('Unauthorized jku'));
}
client.getSigningKey(header.kid, (err, key) => {
if (err) return callback(err);
callback(null, key.getPublicKey());
});
}
// Usage
jwt.verify(token, getKey, { algorithms: ['RS256'] });
Python example with PyJWT and requests that validates the host against an allow‑list:
import jwt
import requests
from urllib.parse import urlparse
TRUSTED_HOSTS = {'login.example.com', 'auth.partner.com'}
def fetch_jwks(url):
parsed = urlparse(url)
if parsed.hostname not in TRUSTED_HOSTS:
raise ValueError(f'Untrusted JWKS host: {parsed.hostname}')
resp = requests.get(url, timeout=5)
resp.raise_for_status()
return resp.json()
def get_key(header, payload):
jku = header.get('jku')
if jku:
jwks = fetch_jwks(jku)
# find matching kid
for key in jwks['keys']:
if key['kid'] == header.get('kid'):
return jwt.algorithms.RSAAlgorithm.from_jwk(key)
raise ValueError('Unable to retrieve signing key')
jwt.decode(token, key=get_key, algorithms=['RS256'])
3. Enforce TLS certificate pinning Ensure that any HTTPS request to a JWKS endpoint validates the server’s certificate chain and, if possible, pins the expected leaf certificate or public key to prevent man‑in‑the‑middle attacks that could arise even when DNS is not dangling.
By applying these controls—disabling unnecessary remote key fetching, restricting URLs to trusted hosts, and validating TLS—you eliminate the attack surface that dangling DNS exploits in JWT processing.