CVE-2026-29000: The pac4j JWT Bypass That Lets Attackers Become Anyone
A critical authentication bypass in pac4j-jwt allows attackers with only the server's RSA public key to forge tokens and impersonate any user, including administrators. Patch immediately.
CVE-2026-29000: The pac4j JWT Bypass That Lets Attackers Become Anyone
In March 2026, security researchers dropped a bombshell on the Java authentication ecosystem: CVE-2026-29000, a maximum-severity vulnerability in pac4j-jwt that allows attackers to authenticate as arbitrary users—including administrators—using nothing more than the server's RSA public key. That's right: a key meant for encryption becomes a weapon for total compromise.
The vulnerability affects pac4j-jwt versions prior to 4.5.9, 5.7.9, and 6.3.3. If you're running affected versions and accepting encrypted JWTs, an attacker can craft a JWE-wrapped PlainJWT with arbitrary claims, bypass signature verification entirely, and walk through your front door as any user they choose.
⚠️ Immediate Action Required
pac4j's own security advisory doesn't mince words: upgrade immediately. This isn't a theoretical flaw—it's an authentication bypass that requires only the public key (which is, by definition, public) to execute. If your system logs show JWE-encrypted JWTs being processed, consider your authentication boundary already breached.
The Attack: When Encryption Becomes Exploitation
The vulnerability exists in pac4j's JwtAuthenticator class when processing encrypted JWTs (JWE). Here's the brutal simplification of how the bypass works:
- Attacker obtains your RSA public key — This is public by design, available in JWKS endpoints, certificates, or API documentation
- Creates a forged JWE token — Encrypts a
PlainJWT(unsigned, with attacker-controlled claims) using your public key - Sends the token to your API — pac4j decrypts it, fails to properly verify that it's actually signed, and accepts the forged claims
- Instant administrator access — If the attacker set
subto an admin user androletoADMIN, they're now an admin
The flaw is in the trust boundary: pac4j assumed that successful decryption implied authenticity. It didn't verify that the inner JWT was properly signed after decryption—a classic authentication bypass pattern.
// VULNERABLE: pac4j-jwt < 4.5.9 / 5.7.9 / 6.3.3
// This is what pac4j was doing - decrypt but not verify signature
// Attacker creates this token locally using YOUR public key:
String maliciousToken = Jwts.builder()
.setSubject("admin@company.com") // Any user they want
.claim("role", "ADMIN") // Any roles they want
.claim("permissions", "*:*") // Wildcard permissions
.encryptWith(rsaPublicKey, JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
.compact();
// Sent to your API: POST /api/protected/resource
// Header: Authorization: Bearer <maliciousToken>
// Result: pac4j decrypts ✓ → trusts claims ✓ → authentication bypass ✓
Impact Assessment: What This Actually Means
Authentication Bypass
Complete bypass of authentication for any user in the system. No password, no MFA, no session required.
Privilege Escalation
Attackers can forge tokens claiming any role or permission level, from basic user to super admin.
Data Exfiltration
With admin-level access, attackers can access any data your API serves, regardless of intended access controls.
Lateral Movement
If your API is part of a microservices mesh, this becomes the beachhead for broader infrastructure compromise.
Detection: Are You Already Compromised?
Check your application logs for these indicators of compromise:
# Log analysis commands to detect potential exploitation
# 1. Look for JWT tokens with unusual claim patterns
grep -i "authorization.*bearer" /var/log/app/*.log | \
jq -R 'split(" ")[-1] | split(".")[1] | @base64d | fromjson' 2>/dev/null | \
jq 'select(.sub | contains("admin") or .role | contains("ADMIN"))'
# 2. Check for tokens with alg: "none" or missing signature after decryption
# (pac4j logs decrypted token structure)
grep -i "jwtauthenticator\|jwe" /var/log/app/*.log | \
grep -E "plainjwt|unsigned|no signature"
# 3. Monitor for rapid authentication success with different subjects
# from same source IP (token enumeration pattern)
awk '/authentication.*success/ {print $1, $2, $(NF-3)}' /var/log/app/auth.log | \
sort | uniq -c | sort -rn | head -20
Look for:
- JWT tokens accepted where the inner payload shows
alg: "none" - Successful authentications with claim values that don't match your user database
- Authentication events from IPs not associated with legitimate user sessions
- Tokens where the
subclaim doesn't correlate with your identity provider's issued tokens
📋 Historical Context: This Isn't pac4j's First JWT Rodeo
CVE-2021-44878 exposed a similar issue: pac4j versions 5.1 and earlier accepted unsigned OpenID Connect ID tokens using the "none" algorithm. The pattern is clear—JWT handling is hard, and library assumptions about trust boundaries are frequently wrong. pac4j addressed that earlier issue in v5.2.0, but CVE-2026-29000 shows that encrypted JWT paths had a parallel vulnerability. When choosing an authentication library, understand not just its current security posture, but its architectural history of trust assumptions.
Remediation: The Only Fix That Matters
Upgrade immediately. There is no configuration workaround that fully addresses this vulnerability.
Affected Versions:
- pac4j-jwt
< 4.5.9 - pac4j-jwt
< 5.7.9 - pac4j-jwt
< 6.3.3
Safe Versions:
- pac4j-jwt
>= 4.5.9 - pac4j-jwt
>= 5.7.9 - pac4j-jwt
>= 6.3.3
<!-- Maven: Update your pom.xml -->
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-jwt</artifactId>
<version>6.3.3</version> <!-- or latest -->
</dependency>
<!-- Gradle: Update your build.gradle -->
implementation 'org.pac4j:pac4j-jwt:6.3.3'
Immediate Mitigation (If you cannot upgrade immediately):
If patching is blocked by release cycles, implement these temporary controls:
// Custom JWT validation wrapper to add signature verification
// Wrap pac4j's JwtAuthenticator with explicit signature checks
@Component
public class HardenedJwtAuthenticator extends JwtAuthenticator {
@Override
public void validate(TokenCredentials credentials, WebContext context,
SessionStore sessionStore) {
String token = credentials.getToken();
// Additional validation: Reject plain JWTs in JWE wrappers
if (isJwe(token)) {
JWEObject jwe = JWEObject.parse(token);
jwe.decrypt(decrypter);
SignedJWT signedJwt = jwe.getPayload().toSignedJWT();
// CRITICAL: Explicitly reject if inner JWT is not signed
if (signedJwt == null) {
throw new CredentialsException(
"CVE-2026-29000 mitigation: Rejecting unsigned JWT inside JWE"
);
}
// Verify signature explicitly
if (!signedJwt.verify(verifier)) {
throw new CredentialsException("Invalid JWT signature");
}
}
super.validate(credentials, context, sessionStore);
}
}
Lessons: Trust Boundaries in JWT Libraries
CVE-2026-29000 reveals a fundamental tension in JWT library design:
- Decryption ≠ Authentication — Just because you successfully decrypted a payload doesn't mean it came from who you think it did
- Library abstraction can hide security gaps — pac4j's high-level API made it easy to use encrypted JWTs without understanding that signature verification was silently skipped
- Public keys are public — Design your threat model assuming attackers have your public key. If that breaks your authentication system, your authentication system is broken
The broader lesson for API security teams: when you delegate authentication to a library, understand exactly what that library does. Read the source. Check the advisories. Don't trust that "industry standard" libraries handle every edge case correctly.
Inspect JWTs Without Leaking Secrets
Testing your JWT implementation? Our client-side decoder lets you inspect headers and payloads—including JWE structure and signature algorithms—without sending your tokens to any server. Verify your mitigation is working without risking your credentials.
Open JWT Decoder →References & Further Reading
- NVD - CVE-2026-29000
- pac4j Security Advisory
- JWT.io - JSON Web Token Introduction
- RFC 7516 - JSON Web Encryption (JWE)
Bottom line: If you're running vulnerable pac4j-jwt versions and accepting encrypted JWTs, assume compromise. Patch today, audit your logs, and never assume decryption implies authenticity.