Introduction
NVD has published CVE-2026-105209, a CVSS 9.6 (CRITICAL), network-exploitable improper authorization vulnerability in ZITADEL, the widely deployed open-source identity and access management (IAM) platform. ZITADEL 3.x builds before 3.4.15 and 4.x builds before 4.17.1 fail to validate the target user's organization when issuing passkey and passwordless enrollment codes. The result: any administrator or service account holding user-write permission in one organization can mint an enrollment code for a user in a completely different organization on the same instance, register their own authenticator, and take over that account.
For multi-tenant SaaS operators and MSPs running a shared ZITADEL instance, this is a worst-case tenant-isolation failure. The blast radius is not one organization — it is every organization on the instance. If you run ZITADEL in a multi-tenant configuration, treat this as an emergency change window: patch immediately, then hunt retroactively for cross-organization enrollment events.
Technical Analysis
Affected products and versions
| Product | Affected Versions | Fixed Versions |
|---|---|---|
| ZITADEL | 3.x before 3.4.15 | 3.4.15 |
| ZITADEL | 4.x before 4.17.1 | 4.17.1 |
The vulnerable code path lives in ZITADEL's passkey / passwordless enrollment-code issuance flow (the user v2 API surface used to create PasskeyRegistrationCode and PasswordlessRegistrationCode objects). Self-hosted and operator-managed deployments are affected; tenants on ZITADEL Cloud should confirm their instance patch status with ZITADEL support.
How the vulnerability works — defender's view of the attack chain
The flaw is a textbook broken object level authorization (BOLA) / tenant-scoping failure:
- Authorization check is scoped wrong. When a caller requests an enrollment code for a target user, ZITADEL validates the caller's user-write permission against the organization supplied in the
x-zitadel-orgidHTTP header — the organization context the caller is acting in. - The target user's organization is never checked. The victim user identified in the request body can belong to any organization on the same instance. Because the permission check only evaluated the header org, an attacker with user-write rights in
Org-Apasses a request targeting a user inOrg-Band the check succeeds. - Enrollment code is issued. ZITADEL returns a valid passkey/passwordless registration code and code ID for the victim user. No interaction with the victim, no email possession, no session required.
- Attacker registers their own authenticator. Using the registration code, the attacker enrolls a passkey or passwordless credential they control against the victim's account — achieving durable account takeover, including against phishing-resistant MFA.
- Persistence and access. The attacker authenticates as the victim with their own authenticator. Because the credential is a legitimately registered passkey, subsequent logins look clean in authentication telemetry.
Exploitation requirements: network access to the ZITADEL API, and an authenticated principal (human admin or machine/service account) with user-write permission in at least one organization on the target instance. There is no requirement for any privilege in the victim's organization — that is precisely the defect.
Exploitation status
As of publication, CVE-2026-105209 has been assigned a CVSS v3.1 base score of 9.6 (CRITICAL) with a NETWORK attack vector. At the time of this writing there is no confirmed in-the-wild exploitation and it has not yet been added to the CISA Known Exploited Vulnerabilities catalog — but given that ZITADEL publishes source, the pre/post-patch diff makes the flaw trivially identifiable to anyone diffing 3.4.14→3.4.15 or 4.17.0→4.17.1. Expect working exploitation logic to circulate quickly. Do not wait for KEV inclusion to patch a CVSS 9.6 tenant-isolation break in your identity plane.
Why passkey takeover is especially dangerous
Organizations adopt passkeys specifically because they are phishing-resistant. This vulnerability weaponizes the passkey enrollment mechanism itself — the strongest credential type in your IAM stack becomes the persistence vehicle. Detection must therefore focus on the enrollment event, not the authentication event, because post-takeover logins will present a valid, legitimately-registered authenticator.
Detection & Response
The highest-fidelity detection signal is an enrollment-code issuance event where the requesting organization context (x-zitadel-orgid) does not match the target user's organization. If you ship ZITADEL API/audit logs to your SIEM (via syslog or your reverse proxy / ingress access logs), you can hunt for this directly. Secondary signals: spikes in enrollment code issuance from a single service account, and passkey registration completions shortly after code issuance originating from unexpected IP space.
Sigma rules
---
title: ZITADEL Passkey or Passwordless Enrollment Code Issuance Burst by Single Principal
id: 3f7a2c91-4b6e-4d85-9a1c-8e2f5b7d0a43
status: experimental
description: Detects a single authenticated principal (admin or service account) issuing an abnormal volume of passkey/passwordless registration codes, consistent with CVE-2026-105209 cross-organization account takeover attempts against ZITADEL before 3.4.15 / 4.17.1.
references:
- https://nvd.nist.gov/vuln/detail/CVE-2026-105209
- https://attack.mitre.org/techniques/T1098/
- https://attack.mitre.org/techniques/T1078/
author: Security Arsenal
date: 2026/04/06
tags:
- attack.persistence
- attack.t1098
- attack.t1078
logsource:
product: linux
service: zitadel
detection:
selection:
event_type|contains:
- 'user.v2.passkey.registration.code.added'
- 'user.v2.passwordless.registration.code.added'
- 'PasskeyRegistrationCode'
- 'PasswordlessRegistrationCode'
condition: selection
falsepositives:
- Bulk legitimate onboarding campaigns generating many enrollment codes
level: medium
---
title: ZITADEL Enrollment Code Request With Mismatched Organization Context
id: 8c1d4e60-2f9b-4a37-b5d6-1e8c3a7f9246
status: experimental
description: Detects ZITADEL user v2 API calls that create passkey or passwordless registration codes where the x-zitadel-orgid header organization differs from the target user's organization — the exact exploitation signature of CVE-2026-105209. Requires reverse proxy / ingress logging that captures the x-zitadel-orgid header and the target user ID.
references:
- https://nvd.nist.gov/vuln/detail/CVE-2026-105209
- https://attack.mitre.org/techniques/T1098/
author: Security Arsenal
date: 2026/04/06
tags:
- attack.persistence
- attack.t1098
- attack.privilege_escalation
logsource:
category: webserver
detection:
selection_uri:
cs-uri|contains:
- '/v2/users/'
selection_method:
cs-method: 'POST'
selection_action:
cs-uri|contains:
- 'passkey'
- 'passwordless'
condition: selection_uri and selection_method and selection_action
falsepositives:
- Legitimate helpdesk-driven passkey enrollment within the caller's own organization (filter where org header matches target user org)
level: high
---
title: Passkey Registration Completion Followed by Authentication From New Source
id: 5b9e3d17-6c4a-48f2-a0d9-7f2e1b6c8530
status: experimental
description: Detects successful ZITADEL authentication using a newly registered passkey/passwordless credential from a source IP or user agent not previously associated with the account, indicating possible post-takeover access following CVE-2026-105209 exploitation.
references:
- https://nvd.nist.gov/vuln/detail/CVE-2026-105209
- https://attack.mitre.org/techniques/T1078/
author: Security Arsenal
date: 2026/04/06
tags:
- attack.initial_access
- attack.t1078
logsource:
product: linux
service: zitadel
detection:
selection:
event_type|contains:
- 'user.v2.passkey.added'
- 'user.v2.passwordless.added'
- 'passkey.login.succeeded'
- 'passwordless.login.succeeded'
condition: selection
falsepositives:
- Users legitimately enrolling and immediately using passkeys on new devices
level: low
KQL — Microsoft Sentinel hunt
This query assumes ZITADEL application and reverse-proxy/ingress logs are ingested into Sentinel via Syslog/CEF. It hunts for enrollment-code creation events and cross-references organization context mismatches, then pivots on burst behavior per caller. Tune the threshold and field names to your ingestion schema.
// Hunt: ZITADEL CVE-2026-105209 — cross-org passkey/passwordless enrollment code abuse
let lookback = 14d;
let EnrollEvents = Syslog
| where TimeGenerated > ago(lookback)
| where SyslogMessage has_any ("PasskeyRegistrationCode", "PasswordlessRegistrationCode", "registration.code.added")
| extend CallerOrg = extract(@"x-zitadel-orgid[:\"= ]+([A-Za-z0-9-]+)", 1, SyslogMessage),
TargetUserOrg = extract(@"user_org[:\"= ]+([A-Za-z0-9-]+)", 1, SyslogMessage),
CallerId = extract(@"caller[:\"= ]+([A-Za-z0-9._@-]+)", 1, SyslogMessage),
TargetUser = extract(@"user_id[:\"= ]+([A-Za-z0-9-]+)", 1, SyslogMessage);
EnrollEvents
| extend OrgMismatch = iff(isnotempty(CallerOrg) and isnotempty(TargetUserOrg) and CallerOrg != TargetUserOrg, 1, 0)
| summarize EventCount = count(), MismatchedOrgEvents = sum(OrgMismatch), DistinctTargets = dcount(TargetUser), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated) by CallerId
| where MismatchedOrgEvents > 0 or EventCount >= 10
| project CallerId, EventCount, MismatchedOrgEvents, DistinctTargets, FirstSeen, LastSeen
| order by MismatchedOrgEvents desc, EventCount desc;
// Secondary: new passkey auth from previously unseen source after any enrollment event
CommonSecurityLog
| where TimeGenerated > ago(lookback)
| where AdditionalExtensions has "passkey.login.succeeded"
| summarize FirstSeenFromSource = min(TimeGenerated), AuthCount = count() by SourceIP, DestinationUserName
| where AuthCount <= 3
| order by FirstSeenFromSource desc;
Velociraptor VQL — version and exposure sweep
Use this hunt artifact across your fleet (or against your container hosts / orchestration nodes) to identify where vulnerable ZITADEL binaries or deployment manifests exist, so remediation can be verified at scale.
-- Hunt for vulnerable ZITADEL deployments (CVE-2026-105209): binary presence, version strings, and running services
LET binaries = SELECT FullPath, Size, Mtime
FROM glob(globs=['/usr/local/bin/zitadel', '/opt/**/zitadel', '/usr/bin/zitadel', '/var/lib/**/zitadel'])
WHERE NOT IsDir;
LET procs = SELECT Pid, Name, CommandLine, Exe, Username, CreateTime
FROM pslist()
WHERE Name =~ '(?i)zitadel' OR CommandLine =~ '(?i)zitadel';
SELECT * FROM binaries
UNION ALL
SELECT Exe AS FullPath, NULL AS Size, CreateTime AS Mtime FROM procs;
Remediation / verification script
Run on each ZITADEL host or against your container registry to confirm patch status, and to grep recent logs for suspicious enrollment-code issuance while you patch.
#!/usr/bin/env bash
# CVE-2026-105209 - ZITADEL cross-org passkey enrollment verification & audit
set -euo pipefail
echo "=== [1] Detect ZITADEL version ==="
if command -v zitadel >/dev/null 2>&1; then
VER=$(zitadel --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
elif command -v docker >/dev/null 2>&1 && docker ps --format '{{.Image}}' | grep -qi zitadel; then
IMG=$(docker ps --format '{{.Image}}' | grep -i zitadel | head -n1)
VER=$(echo "$IMG" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
echo "Container image: $IMG"
else
echo "ZITADEL not found locally - check your orchestrator (k8s: kubectl get deploy -A -o wide | grep -i zitadel)"
VER=""
fi
echo "Detected version: ${VER:-unknown}"
# Vulnerable: 3.x < 3.4.15, 4.x < 4.17.1
if [ -n "${VER:-}" ]; then
MAJOR=$(echo "$VER" | cut -d. -f1); MINOR=$(echo "$VER" | cut -d. -f2); PATCH=$(echo "$VER" | cut -d. -f3)
VULN=0
if [ "$MAJOR" -eq 3 ]; then
if [ "$MINOR" -lt 4 ] || { [ "$MINOR" -eq 4 ] && [ "$PATCH" -lt 15 ]; }; then VULN=1; fi
elif [ "$MAJOR" -eq 4 ]; then
if [ "$MINOR" -lt 17 ] || { [ "$MINOR" -eq 17 ] && [ "$PATCH" -lt 1 ]; }; then VULN=1; fi
elif [ "$MAJOR" -lt 3 ]; then
VULN=1
fi
if [ "$VULN" -eq 1 ]; then
echo "[!] VULNERABLE to CVE-2026-105209 - upgrade to 3.4.15 (3.x) or 4.17.1 (4.x) IMMEDIATELY"
else
echo "[+] Version appears patched."
fi
fi
echo "=== [2] Audit recent logs for enrollment-code issuance (last 24h) ==="
LOGSRC="/var/log"
journalctl -u zitadel --since "24 hours ago" --no-pager 2>/dev/null | grep -iE "PasskeyRegistrationCode|PasswordlessRegistrationCode|registration.code" || \
grep -riE "PasskeyRegistrationCode|PasswordlessRegistrationCode|registration.code" "$LOGSRC" 2>/dev/null | tail -n 50 || \
echo "No local enrollment events found - verify via SIEM/ingress logs"
echo "=== [3] If running behind a reverse proxy, confirm x-zitadel-orgid is logged ==="
echo "Add to nginx ingress: log_format with \$http_x_zitadel_orgid"
echo "=== [4] Remediation ==="
echo "Binary: download 3.4.15 / 4.17.1 from https://github.com/zitadel/zitadel/releases"
echo "Docker: docker pull ghcr.io/zitadel/zitadel:v4.17.1 && redeploy"
echo "K8s: helm upgrade zitadel zitadel/zitadel --set image.tag=v4.17.1 -n zitadel"
Remediation
- Patch immediately. Upgrade ZITADEL to 3.4.15 (3.x track) or 4.17.1 (4.x track). There is no configuration workaround that fully closes the tenant-isolation gap — the authorization check itself is defective. Releases are available from the official ZITADEL GitHub releases page and the project's security advisories; consult the NVD entry at https://nvd.nist.gov/vuln/detail/CVE-2026-105209 for canonical references.
- Retroactive hunt before and after patching. Because exploitation leaves a clean credential behind, patching alone does not evict an attacker. Audit all passkey/passwordless enrollment-code issuance events for the last 30–90 days (or your log retention window) and flag any event where the caller's organization context differs from the target user's organization.
- Revoke suspicious credentials. For every anomalous enrollment, invalidate the enrolled passkey/passwordless authenticator, force a credential reset through a verified out-of-band channel, and review that account's session and audit history for post-takeover activity.
- Least privilege on user-write. Enumerate every human admin and machine account holding user-write permission in any organization on the instance. This vulnerability makes each one a potential cross-tenant pivot. Scope user-write to the minimum set of principals, and consider temporarily restricting enrollment-code issuance to a centralized identity team until patching is complete.
- Instrument the header. Ensure your reverse proxy / ingress logs capture the
x-zitadel-orgidheader alongside the request path and authenticated principal. Without this, retrospective cross-org mismatch detection is not possible. - Monitor for KEV inclusion. Although not currently listed in CISA's Known Exploited Vulnerabilities catalog, a CVSS 9.6 network-exploitable IAM flaw with public source diffs is a strong candidate. Track the NVD entry and CISA KEV, and treat inclusion as a federal-compliance remediation deadline trigger if applicable.
Related Resources
Security Arsenal Penetration Testing Services AlertMonitor Platform Book a SOC Assessment vulnerability-management Intel Hub
Is your security operations ready?
Get a free SOC assessment or see how AlertMonitor cuts through alert noise with automated triage.