Feb 17, 2026·6 min read·303 visits
Keycloak forgot to verify JWT signatures in the organization invitation flow. Attackers can take a valid invite token, change the organization ID in the payload to whatever they want, and self-register into restricted organizations without a valid key. It's a high-severity bypass (CVSS 8.1) affecting versions prior to late January 2026.
A critical lapse in cryptographic hygiene within Keycloak's 'Organizations' feature allows attackers to forge invitation tokens. By neglecting to verify the digital signature of JSON Web Tokens (JWTs), Keycloak inadvertently permitted anyone with a valid invite to modify the payload—swapping organization IDs and email addresses—to gain unauthorized access to restricted tenants.
Keycloak is the behemoth of the open-source Identity and Access Management (IAM) world. It’s the gatekeeper we trust to hold the keys to the kingdom, handling everything from OAuth flows to user federation. Recently, Keycloak introduced a shiny new feature called Organizations, designed to support multi-tenancy and B2B SaaS scenarios. It allows admins to invite users to specific organizations via email links containing a token.
But here's the kicker: for a brief, terrifying window in versions 26.x, the gatekeeper stopped checking IDs. When processing these invitation tokens, the system assumed that if a token looked like a JWT, it must be valid. It didn't bother checking the cryptographic signature—the wax seal that proves the letter wasn't tampered with.
This isn't just a minor bug; it's a fundamental breakdown of the JWT trust model. In the security world, we call this "signature exclusion" or "algorithm confusion's" dumber cousin: simply ignoring the signature entirely. It turned Keycloak's robust security model into a purely advisory suggestion, allowing anyone with a text editor to rewrite their own access permissions.
To understand why this is hilarious (in a dark way), you have to understand how Keycloak handles tokens. Usually, the code is paranoid. It checks expiration, issuers, audiences, and crucially, the cryptographic signature using the realm's public key. If a single bit is out of place, the token is rejected.
However, in the org.keycloak.organization.utils.Organizations class, the developers got a little lazy—or perhaps just distracted. The method parseInvitationToken was tasked with reading the token from the URL query parameters. Instead of invoking the full verification pipeline, the code simply deserialized the Base64-encoded payload directly into a Java object.
It’s the digital equivalent of a bouncer looking at a driver's license, seeing that it's a rectangular piece of plastic with a photo, and letting you in—without noticing that the name is written in Crayon and the birth year is 3025. Because the signature verification step (verifier.verify()) was completely omitted, the server accepted any JSON blob formatted as a token, regardless of who signed it (or if it was signed at all).
Let's look at the crime scene. The vulnerable code resided in org.keycloak.organization.utils.Organizations.java. Here is the logic that essentially rolled out the red carpet for attackers:
// THE VULNERABLE CODE
public static InviteOrgActionToken parseInvitationToken(HttpRequest request) throws VerificationException {
// ... query param extraction ...
String tokenFromQuery = queryParameters.getFirst(Constants.TOKEN);
// LOOK HERE: create() parses the text, getToken() returns the object.
// There is NO .verify() call. There is NO signature check.
return TokenVerifier.create(tokenFromQuery, InviteOrgActionToken.class).getToken();
}The fix, introduced in commit 8fc9a98026106a326f4faa98d4c9a48341ace2d7, is a lesson in how verbose Java security can get. They had to wire in the KeycloakSession, fetch the SignatureVerifierContext, and explicitly force the check:
// THE PATCHED CODE
public static InviteOrgActionToken parseInvitationToken(KeycloakSession session, HttpRequest request) throws VerificationException {
// ... setup ...
TokenVerifier<InviteOrgActionToken> verifier = TokenVerifier.create(tokenFromQuery, InviteOrgActionToken.class)
.withChecks(TokenVerifier.IS_ACTIVE, /* URL checks */);
// Fetch the realm's keys to verify the signature
SignatureVerifierContext verifierContext = session.getProvider(SignatureProvider.class,
verifier.getHeader().getAlgorithm().name())
.verifier(verifier.getHeader().getKeyId());
verifier.verifierContext(verifierContext);
// The magic word: .verify()
return verifier.verify().getToken();
}The difference is subtle in lines of code but infinite in security implications. The first version trusts the input; the second version treats the input like a hostile combatant until proven otherwise.
Exploiting this requires zero advanced hacking tools. You don't need heap spraying or ROP chains; you just need a browser developer console or a simple Python script. The attack vector relies on the fact that a JWT is just Header.Payload.Signature. Since the server ignores the Signature, we can change the Payload and leave the Signature as garbage (or keep the original, it doesn't matter).
Step 1: The Setup.
The attacker needs a valid invitation link. They can invite themselves to a "dummy" organization they control to get a valid token format. The link looks like https://auth.target.com/realms/main/protocol/openid-connect/registrations?token=eyJhbG...
Step 2: The Tampering. The attacker takes the token string. The middle part (between the two periods) is the Base64-encoded payload. They decode it and see JSON:
{
"iss": "https://auth.target.com/realms/main",
"orgId": "b-123-useless-org",
"email": "attacker@evil.com",
"exp": 1790000000
}They change "orgId" to the ID of the Executive Org (e.g., "a-999-super-secret-admin-org") which they might have enumerated via other API leaks or guessed. They ensure the email matches the account they want to register.
Step 3: The Delivery.
The attacker re-encodes this JSON to Base64 and pastes it back into the token string. They send the request. Keycloak's parseInvitationToken method deserializes the forged payload, sees the target orgId, assumes it's valid because it deserialized correctly, and grants the attacker membership to the target organization. Congratulations, you are now an employee.
The impact here is a complete violation of multi-tenancy. In a SaaS platform using Keycloak Organizations, this vulnerability allows cross-tenant access. An attacker can move laterally from a low-value demo organization into a high-value corporate organization simply by knowing the target's ID.
Depending on how the application consumes these organizational memberships, this could lead to:
This scores an 8.1 on CVSS not because it crashes the server, but because it destroys the integrity of the access control model. Confidentiality and Integrity are both rated 'High' for a reason.
Red Hat and the Keycloak maintainers pushed a fix in late January 2026. The remediation is straightforward: Update Keycloak immediately.
If you are running Keycloak version 26.2.x, upgrade to 26.2.13-1 or later. If you are running version 26.4.x, upgrade to 26.4.9-1 or later.
If patching is currently impossible due to bureaucratic inertia or change freezes, there is a configuration workaround: Disable the Organizations feature. This feature is often guarded by a feature flag. Turning it off removes the vulnerable endpoint entirely. However, if your business logic relies on this feature, you are effectively stuck between a rock and a hard place until you patch.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Keycloak Red Hat | 26.2.0 - 26.2.12 | 26.2.13-1 |
Keycloak Red Hat | 26.4.0 - 26.4.8 | 26.4.9-1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-347 (Improper Verification of Cryptographic Signature) |
| CVSS v3.1 | 8.1 (High) |
| Attack Vector | Network (AV:N) |
| Privileges Required | Low (PR:L) |
| Exploit Status | PoC Available |
| EPSS Score | 0.00016 (Rising) |
| Impact | High Integrity & Confidentiality Loss |
The product does not verify, or incorrectly verifies, the cryptographic signature for data, allowing the data to be modified without detection.
A high-severity access control vulnerability in ToolHive CLI before v0.30.1 and ToolHive Studio before v0.38.0 allows local containerized MCP servers to bypass network isolation. This enables malicious workloads to establish TCP/IP connections to administrative and control plane endpoints exposed on the host loopback interface.
AnyCable is a real-time communication server. Prior to version 1.6.15, its Pusher-compatible REST API suffered from an authentication bypass vulnerability because it failed to verify that the request body matched the signature-validated body_md5 parameter. This allows attackers to perform replay attacks with modified body contents.
A denial-of-service vulnerability exists in AnyIO prior to version 4.14.2. Standard error streams of process-pool workers are connected to an operating system pipe that is never drained by the parent process. This allows a worker to fill the pipe buffer and deadlock indefinitely.
CVE-2026-63349 is a critical privilege-dropping bypass vulnerability in the AnyIO asynchronous framework (versions 4.14.0 and 4.14.1) on POSIX platforms. Due to a variable assignment typo, supplementary groups specified by the developer are not correctly propagated to the execution backend, resulting in subprocesses retaining the parent process's elevated supplementary group permissions.
CVE-2026-63406 is an information disclosure vulnerability in AnyCable-go prior to version 1.6.15. The built-in telemetry client is enabled by default with a hardcoded public authentication token ('secret'). This client digests highly sensitive configuration parameters and command-line arguments, including JWT secrets and RPC secrets, into a stable SHA-256 fingerprint. This fingerprint is sent over public networks, exposing those administrative secrets to offline dictionary and brute-force attacks if intercepted.
CVE-2026-84992 is a Cross-Site Scripting (XSS) vulnerability affecting md-editor-v3 before version 6.5.4. It occurs because the fenced-code block language parser directly interpolates unescaped language metadata into unquoted HTML attributes inside the custom rendering callback. This bypasses the built-in XSSPlugin which runs during the parsing phase, before rendering.