OpenZiti edge router MFA posture nil-pointer panic causing authenticated router DoS

Summary

The OpenZiti edge router evaluates posture checks locally when an OIDC/JWT-session client dials or
binds a service. The router's MFA posture check (router/posture/mfa.go, MfaCheck.Evaluate)
reads state.Woken.Time and state.Unlocked.Time without checking whether state.Woken /
state.Unlocked are nil. When an administrator has enabled promptOnWake and/or promptOnUnlock
on an MFA posture check, and an authorized client has passed MFA but has not reported a
wake/unlock event (the normal default state), the field is nil and evaluation panics. The panic
occurs in an unrecovered goroutine, crashing the entire edge-router process (data-plane DoS). The
controller's equivalent check treats nil wake/unlock timestamps as "pass", so the router behavior
is a semantic-mismatch bug.

Affected Component

  • Module/package: router/posture (edge router data-plane authorization).
  • File/function: router/posture/mfa.go, func (m *MfaCheck) Evaluate(state *InstanceData) *CheckError.
  • Lines: 53–63 (PromptOnWakestate.Woken.Time.AsTime()), 65–76 (PromptOnUnlock
    state.Unlocked.Time.AsTime()).

Vulnerability Class

  • Primary: CWE-476 NULL Pointer Dereference.
  • Secondary: CWE-248 Uncaught Exception (panic crosses an unrecovered goroutine boundary →
    process termination).

Attacker Model

An authenticated, enrolled client identity that:

  • holds an OIDC/JWT API session (the modern 2.x/HA default), and
  • is authorized by policy to dial or bind a service that an administrator has guarded with an MFA
    posture check having promptOnWake and/or promptOnUnlock enabled, and
  • has passed MFA but has not reported a wake/unlock posture event.

This is NOT an unauthenticated or remote-anonymous DoS. The attacker uses access the policy
already grants and a normal MFA login; it then triggers an existing administrator-enabled posture
configuration.

Preconditions

  • An MFA posture check with promptOnWake=true and/or promptOnUnlock=true is associated (via a
    service policy) with at least one service. These are documented, supported options
    (CHANGELOG.0.20), configured by an administrator.
  • An enrolled client with an OIDC/JWT session and a dial/bind authorization path to that service.
  • The client has passed MFA (PassedMfaAt set) and has not reported a wake/unlock event
    (Woken/Unlocked nil) — the normal default for a client that has not slept/locked.

Root Cause

MfaCheck.Evaluate assumes state.Woken / state.Unlocked are always populated when the
corresponding prompt flag is set, and dereferences them unconditionally. They are independent,
optional posture fields (PostureResponse_Woken / PostureResponse_Unlocked) that default to nil
and are only set when the SDK reports a wake/unlock. Passing MFA sets only PassedMfaAt, not these
fields. The controller's reference implementation
(controller/model/posture_check_model_mfa.go, PassedOnWake / PassedOnUnlock) explicitly
treats nil wake/unlock timestamps as "pass" and never dereferences a nil — the router diverges.

Impact

  • Deterministic, whole-process edge-router crash (availability impact); all circuits and sessions
    carried by the router are dropped, and the crash recurs whenever the same access path is
    exercised.
  • Triggered by an authenticated/enrolled client under an administrator-enabled MFA-on-wake/unlock
    posture configuration.
  • No confidentiality or integrity impact; no auth/MFA bypass, service-access gain, RCE, data
    theft, or controller compromise is claimed or demonstrated.
  • Conservative severity: Medium (indicative CVSS 3.1 AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H ≈ 6.5).

Package

ziti

Affected Versions

0.34.x,1.1.x,1.5.x,1.6.x,2.0.x

Patched Versions

1.6.18, 2.0.2