#Security#JWT#Keycloak#Authentication#OAuth

CVE-2026-1529: 'Decodable' and 'Verified' Are Not the Same Thing

webhani·

What CVE-2026-1529 is

A high-severity vulnerability (CVSS 8.1) was disclosed in Keycloak affecting its organization invitation feature. The root cause: the code parsing invitation JWTs never verified the cryptographic signature.

The flaw lived in org.keycloak.organization.utils.Organizations.parseInvitationToken, which decoded the invitation token's JWT payload without checking its signature. With one legitimate invitation token in hand, an attacker could simply edit the organization ID (org_id) and target email (eml) fields in the payload and self-register into an organization they were never invited to.

Webhani regularly reviews authentication implementations for clients, and this class of bug isn't an exotic attack technique — it's a pattern that recurs in JWT-handling code. Here's the general lesson worth pulling out of it.

Why this pattern keeps showing up

By design, a JWT's payload is just Base64-encoded and readable by anyone who decodes it. That "readable" property and "guaranteed unmodified" are two completely different guarantees — you only get the second one once you've verified the signature.

import jwt
 
# Dangerous: decoding without signature verification
payload = jwt.decode(token, options={"verify_signature": False})
org_id = payload["org_id"]  # trusting a value the attacker can freely rewrite

This code looks like it's "handling a JWT," but functionally it's identical to trusting whatever value comes out of a Base64 string, unconditionally. It's easy for code like this to slip into a quickly-built invitation flow, password reset, or email confirmation feature — anywhere a one-time token gets rolled by hand.

The correct implementation pattern

Always route through signature verification, and never touch a payload that failed it:

import jwt
from jwt.exceptions import InvalidSignatureError, ExpiredSignatureError
 
try:
    payload = jwt.decode(
        token,
        key=PUBLIC_KEY,
        algorithms=["RS256"],  # pin the algorithm explicitly
        options={"require": ["exp", "org_id", "eml"]},
    )
except (InvalidSignatureError, ExpiredSignatureError):
    raise PermissionError("Invalid invitation token")
 
org_id = payload["org_id"]  # only trusted after signature verification

Pinning the algorithms parameter explicitly matters too. Code that leaves the algorithm unspecified, or accepts alg: none, opens the door to a separate class of attack (algorithm confusion) that effectively disables signature verification altogether.

A checklist for invitation and token-based features

When Webhani reviews a client's authentication layer, here's what we check specifically for invitation links and one-time tokens:

  • Is signature verification enforced on every code path? — audit every place a token gets decoded to confirm none of them skip straight to deserialization
  • Is the algorithm pinned explicitly? — do the allowed algorithms at issuance match the allowed algorithms at verification?
  • Are critical payload fields re-checked against server-side state? — values like organization ID or target email shouldn't just pass signature verification; re-confirm them against the server's own record after decoding
  • Are tokens one-time and time-bound? — is there a mechanism (used-flag, short expiry) that invalidates a token after it's consumed once?

Why using a mature IdP doesn't make this someone else's problem

"Our auth layer runs on a proven OSS product like Keycloak, so we're covered" is exactly the assumption this case breaks. A vulnerability in the IdP product itself becomes your service's risk the moment you miss the patch window.

  • Build a habit of periodically checking security advisories for whatever IdP product you depend on
  • If you've layered custom logic on top of the invitation or registration flow, check whether your own assumptions still hold after the IdP patches its side
  • For a CVSS 8.1-class vulnerability, don't wait for your normal patch cadence — keep a separate emergency-patch path ready to go

Takeaway

CVE-2026-1529 is a bug in one specific product, but it's also a textbook example of a pitfall common to any JWT-handling code. "Decodable" and "verified" are not the same thing — a basic principle, but one that's easy to drop precisely when a team rushes through a "minor" feature like an invitation flow or one-time link. Webhani continues to check that this fundamental discipline holds whenever we review a client's authentication implementation.


Sources: CVE-2026-1529 (Red Hat Customer Portal), CVE-2026-1529 Forged invitation JWT enables cross-organization self-registration (GitHub keycloak/keycloak)