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 (
PromptOnWake→state.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 havingpromptOnWakeand/orpromptOnUnlockenabled, 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=trueand/orpromptOnUnlock=trueis 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 (
PassedMfaAtset) and has not reported a wake/unlock event
(Woken/Unlockednil) — 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