Back to Intelligence

CVE-2026-105209: Critical ZITADEL Cross-Organization Passkey Takeover — Detection and Remediation Guide

SA
Security Arsenal Team
October 4, 2026
11 min read

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

ProductAffected VersionsFixed Versions
ZITADEL3.x before 3.4.153.4.15
ZITADEL4.x before 4.17.14.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:

  1. 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-orgid HTTP header — the organization context the caller is acting in.
  2. 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-A passes a request targeting a user in Org-B and the check succeeds.
  3. 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.
  4. 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.
  5. 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

YAML
---
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.

KQL — Microsoft Sentinel / Defender
// 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.

VQL — Velociraptor
-- 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.

Bash / Shell
#!/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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Instrument the header. Ensure your reverse proxy / ingress logs capture the x-zitadel-orgid header alongside the request path and authenticated principal. Without this, retrospective cross-org mismatch detection is not possible.
  6. 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.