Skip to content

fix(gateway): isolate _auth_env under multiplex to prevent cross-profile allowlist leak (#80026) - #80238

Closed
RelaxJonh wants to merge 2 commits into
NousResearch:mainfrom
RelaxJonh:fix/auth-env-multiplex-isolation-80026
Closed

fix(gateway): isolate _auth_env under multiplex to prevent cross-profile allowlist leak (#80026)#80238
RelaxJonh wants to merge 2 commits into
NousResearch:mainfrom
RelaxJonh:fix/auth-env-multiplex-isolation-80026

Conversation

@RelaxJonh

Copy link
Copy Markdown
Contributor

Fix: Isolate _auth_env under multiplex to prevent cross-profile allowlist leak

Fixes #80026

Root Cause

_auth_env() in gateway/authz_mixin.py reads allowlist/auth environment variables used by the gateway's second authorization gate. When the profile's secret scope does not contain a requested key, it falls back to os.getenv() — the process-global environment.

Under multiplexed deployments, the process env holds first-writer-bridged values from whichever profile armed its YAML→env bridge first. This causes profile B's authorization gate to consume profile A's allowlist values (e.g. DISCORD_ALLOWED_USERS, GATEWAY_ALLOWED_USERS).

Fix

Apply the same multiplex isolation guard already used by _platform_gate_env() (lines 46-73): when a profile scope is installed AND is_multiplex_active(), absent keys return default instead of falling through to os.environ.

Single-profile deployments (no scope installed, or multiplex off) retain the legacy os.getenv() fallback — behavior-neutral for the common case.

Verification

…ile allowlist leak (NousResearch#80026)

_auth_env() fell back to os.getenv() when the profile secret scope did
not contain the requested key. Under multiplex, process-global environ
may hold another profile's first-writer-bridged allowlist value, causing
profile B's authorization to consume profile A's allowlist.

Apply the same multiplex guard already used by _platform_gate_env():
when a scope is installed AND multiplex is active, absent keys return
default instead of falling through to os.environ.

Fixes NousResearch#80026
@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/gateway Gateway runner, session dispatch, delivery area/auth Authentication, OAuth, credential pools P2 Medium — degraded but workaround exists sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Aug 6, 2026
@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

The authorization helper still allows a shared process-environment allowlist or allow-all value to influence an active multiplexed lookup when profile-scope resolution fails. In multiplexed operation, every scope-resolution failure must return the supplied default; only the legacy non-multiplexed path may consult the process environment. Until that fail-closed behavior is enforced, one profile can still authorize a sender using another profile’s bridged value.

Security evidence:

  • trust boundary: Inbound platform events carry a profile identity into gateway authorization. Multiplex handlers install a profile’s secret mapping, while the process environment is shared and may contain another profile’s bridged authorization values.
  • source/sink/invariant: The change returns the default for an absent key in an installed active scope, but the shared-environment fallback remains reachable when scoped resolution fails. The invariant must cover missing scopes and all scope-resolution failures, not only a normal scoped miss.
  • current-main reproduction: A current-main reproduction showed a shared allow-all value on an active scoped miss; source review shows the same fallback path remains reachable when no usable scope is available.
  • PR-head or patch-replay validation: The reviewed change was checked for scoped hits, scoped misses, and legacy single-profile fallback; the unresolved failure path is in the same authorization helper.
  • positive/negative cases: A scoped value must win over a conflicting process value; an absent or failed active scope must return the supplied default; non-multiplexed resolution may retain the process-environment fallback.
  • residual bypass search: Authorization reads continue through the helper, so the unresolved fallback remains on the common path for platform and global allowlist and allow-all decisions.
  • reviewer validation: The scope contract and authorization call graph require active multiplex lookups to fail closed on resolution failures; the current change does not enforce that invariant for every failure path.

Not checked:

  • gateway authorization regression checks
  • external-secret scope checks

Signed: GPT-5.6-luna-max in Codex

…ate is fixed

The _auth_env isolation in authz_mixin.py now correctly prevents
cross-profile env leakage under multiplex, so the previously-expected-fail
test test_scope_does_not_inherit_environ_opt_in passes.  Remove the
xfail(strict=True) marker so CI goes green.
@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

The new head closes the ordinary installed-scope miss and makes the previously xfailed isolation test pass, but the earlier request to fail closed for a missing or failed scope is still open. With multiplexing active and QQ_ALLOW_ALL_USERS present only in the shared process environment, the current change returns the safe default for an empty installed scope, yet still returns the shared value when scope.get(...) raises or when no scope is installed.

Security evidence:

  • trust boundary: Gateway authorization relies on a per-profile secret scope, while the process environment is shared across multiplexed profiles and can contain another profile's bridged authorization settings.
  • source/sink/invariant: _auth_env feeds platform and global allowlist/allow-all decisions. Active multiplex lookups must never fall through to the shared environment when profile-scope resolution is missing or fails.
  • current-main reproduction: The shared authorization value is reachable through the helper before the change for ordinary scope misses as well as missing and raising scopes.
  • PR-head or patch-replay validation: The current change fixes the ordinary empty-scope case, but exact-head validation still returned the shared value for a raising scope and for no installed scope.
  • positive/negative cases: Scoped hits and ordinary scoped misses behave correctly, and single-profile fallback is preserved; missing-scope and raising-scope multiplex cases remain fail-open.
  • residual bypass search: The broad exception handler in gateway/authz_mixin.py still reaches the final os.getenv fallback, including when get_secret raises its multiplex UnscopedSecretError.
  • reviewer validation: The focused QQ scope and adjacent multiplex authorization tests pass, but direct failure-path validation still reproduces both unresolved cases. Please keep the environment fallback exclusively on the non-multiplex path and add missing-scope plus raising-scope regressions.

Not checked:

  • Full gateway test suite
  • External-secret scope integrations

Signed: GPT-5.6-sol-xhigh in Codex

@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

The proposed scoped-miss fix prevents a profile scope from inheriting another profile's authorization values, but multiplex authorization still falls back to the process environment when no profile scope is installed. The secondary busy-session callback, startup/reconnect resume validation, adapter-side sender checks, and voice authorization invoke authorization outside the routed profile scope, so a process-global allowlist or allow-all setting can authorize an event under the wrong profile. Keep this change blocked until those flows run inside the routed profile scope or the helper fails closed when multiplexing has no active scope.

  • [P1] P1: unscoped multiplex authorization can use process-global values

The authorization helper must use only the routed profile's scope while multiplexing. A missing scope must not read process-global values; single-profile fallback remains allowed. Apply the routed scope to busy-session authorization, startup/reconnect resume-owner validation, and adapter-side sender and voice authorization callbacks, or make the helper fail closed when no scope is active, and add regression coverage for these flows with an empty scope and conflicting process values.

Security evidence:

  • trust boundary: Untrusted platform events provide user, chat, and profile-routed session inputs to gateway authorization. Profile allowlists and allow-all settings are trust decisions; process-global values can belong to another profile.
  • source/sink/invariant: Multiplex authorization must read keys only from the routed profile scope. A scoped miss and a missing scope must deny or return the default, while single-profile fallback remains available.
  • current-main reproduction: On current main, an active empty profile scope with a conflicting platform allow-all value authorizes the sender through process-global fallback. The proposed change blocks that scoped miss, but an active multiplex run with no scope still authorizes through the same fallback.
  • PR-head or patch-replay validation: The proposed change makes an installed scope authoritative and preserves single-profile fallback. The normal profile message handler is scoped, while busy-session, startup/reconnect resume, and adapter-side sender and voice authorization callbacks can execute without that scope.
  • positive/negative cases: Scoped platform authorization is preserved, an empty active scope blocks a conflicting process value, and single-profile process-environment fallback remains available. The no-scope multiplex case remains contrary to the fail-closed invariant.
  • residual bypass search: Busy-session, startup/reconnect resume, and adapter-side sender and voice authorization callbacks remain unscoped; the normal profile message handler is scoped.
  • reviewer validation: Scoped authorization and single-profile fallback checks pass.

Not checked:

  • full gateway QQ scope-path integration test
  • networked adapter exercise

Signed: GPT-5.6-luna-max in Codex

@teknium1

Copy link
Copy Markdown
Contributor

Implemented on main via #93446 (2912c36): _auth_env now delegates to _platform_gate_env, so a scoped miss under multiplex returns the default instead of falling through to os.environ — the exact cross-profile allowlist leak this PR fixes. Single-profile deployments keep the legacy os.getenv read. Closing as resolved; thanks for the fix and the clear root-cause, @RelaxJonh.

@teknium1 teknium1 closed this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Discord multiplex gateway second auth gate can apply another profile’s allowlist after #75970

4 participants