ZAC Node Server /sso Endpoint Allowing Unauthenticated Admin Session-Token Theft

Summary

The ZAC Node backend exposes an unauthenticated GET /sso endpoint that creates a fully authenticated ZAC session from query parameters, with no allowlist check on the target controller. Because the Node server holds the controller connection in process-global variables shared across all users, an unauthenticated attacker can both log in and redirect every user's upstream controller URL — causing a logged-in administrator's controller session token to be forwarded to the attacker.

Affected Component

  • Package: openziti/ziti-console (Ziti Admin Console) — Node backend
  • File: server.js — the /sso handler
  • Root of the cross-user effect: the process-global baseUrl / serviceUrl connection state

Vulnerability Class

  • Primary: CWE-306 — Missing Authentication for Critical Function
  • Secondary: CWE-384 — Session Fixation; CWE-441 — Unintended Proxy or Intermediary ('Confused Deputy')

Attacker Model

An unauthenticated remote attacker with network access to the ZAC Node server. No credentials are required to create a session or to redirect the shared controller URL; the token-theft escalation additionally requires a logged-in administrator to make a request while the upstream is redirected.

Preconditions

ZAC running via the Node backend (server.js) in node-api or classic mode (the default container entrypoint). Deployments that serve only the static Angular application are not affected.

Root Cause

GET /sso creates an authenticated ZAC session from query parameters and, unlike POST /api/login, performs no allowlist check on the target controller. Because the Node server stores the controller connection in process-global variables (baseUrl / serviceUrl) shared across all users, whoever calls /sso last controls where every user's authenticated requests are sent.

Impact

An unauthenticated remote attacker can (1) obtain a logged-in ZAC session, and (2) redirect the upstream controller URL to a host they control. On the next request from any logged-in administrator, ZAC forwards that admin's controller session token to the attacker's host. This can result in compromise of admin sessions and grant an attacker access to the management plane.

  • CVSS v3.1: 9.0 (Critical)
  • Vector: AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H

Not impacted: static-only (SPA) deployments that never run server.js.

Package

openziti/ziti-console (app-ziti-console, Node backend / node-api mode)

Affected Versions

  • app-ziti-console >= 2.6.4, < 4.5.3

Patched Versions

  • app-ziti-console 4.5.3

Remediation

Upgrade to 4.5.3, which removes the unused /sso handler; the data-proxy endpoints were additionally hardened to resolve the upstream controller from the server-side allowlist rather than shared global state. If you cannot upgrade, block GET /sso at a reverse proxy, or run ZAC in static-asset (SPA) mode instead of the Node proxy.

References

Credits

Reported by dmuensterer and sajdakabir.

Can you please share some example configuration snippets?

@frm you mean to use ZAC hosted by the controller? "in static-asset (SPA) mode" as Ryan put it.

With 2.1.x you can run ziti run quickstart --zac which will bring zac down for you. When you look at the controller config file you'll see this entry under the web bindings:

      - binding: spa
        options:
          path: zac
          location: C:/Users/clint/AppData/Local/Temp/quickstart3731527080/console
          indexFile: index.html

So get your ZAC, unzip it somewhere and add that entry.and you'll have access to zac at /zac on your controller:

(if that's what you meant)