Skip to content

fix(gateway): route platform authorization reads through the profile secret scope - #93605

Closed
aniruddhaadak80 wants to merge 1 commit into
NousResearch:mainfrom
aniruddhaadak80:fix/platform-authz-scope
Closed

aniruddhaadak80 wants to merge 1 commit into
NousResearch:mainfrom
aniruddhaadak80:fix/platform-authz-scope

Conversation

@aniruddhaadak80

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes a multiplexed-profile isolation break in platform authorization (issue #93522). Under gateway.multiplex_profiles, every secondary profile is constructed inside _profile_runtime_scope and its .env lives in that profile's secret scope — gateway/run.py explicitly does not mutate os.environ with it. Four adapters still read their authorization config via raw os.getenv, producing two failure modes for any non-default profile using them:

  1. Fail-closed silent rejection — profile B sets WEIXIN_ALLOWED_USERS=alice (env-only) → raw getenv misses it → intake drops every DM/group message with no error, before the runner-level scoped authz check ever runs.
  2. Fail-open cross-profile leak — the default profile's GATEWAY_ALLOW_ALL_USERS=true / allowlists live in the shared process env and leak into B's admission gates.

What changed

Location Reads routed through the scoped resolver
gateway/platforms/weixin.py WEIXIN_DM_POLICY, WEIXIN_ALLOWED_USERS, WEIXIN_GROUP_ALLOWED_USERS, WEIXIN_ALLOW_ALL_USERS, GATEWAY_ALLOW_ALL_USERS (via existing _wx_secret)
gateway/platforms/yuanbao.py new _yb_secret() helper; YUANBAO_DM_POLICY, YUANBAO_DM_ALLOW_FROM, YUANBAO_GROUP_POLICY, YUANBAO_GROUP_ALLOW_FROM; AccessPolicy._open_dm_opted_in
gateway/platforms/signal.py new _sig_secret() helper; SIGNAL_GROUP_ALLOWED_USERS, SIGNAL_ALLOWED_USERS
plugins/platforms/wecom/adapter.py existing _get_scoped_secret extended to the authz reads beside the already-scoped credential reads
gateway/run.py::_own_policy_open_startup_violation GATEWAY_ALLOW_ALL_USERS now via scoped _getenv, matching its sibling dm/group reads

All reads use the canonical fail-closed shape QQ's _resolve_qq_secret already established: scope hit wins; with no scope installed (single-profile deployments), legacy os.environ behavior is preserved exactly. This is the authorization-axis counterpart of #59662/#59739/#86905's credential work, per AGENTS.md §Profiles rule 7 which names {PLATFORM}_ALLOW_ALL_USERS / {PLATFORM}_ALLOWED_USERS / GATEWAY_ALLOW_ALL_USERS explicitly as must-be-scope-aware.

Related Issue

Fixes #93522

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • 🧪 Tests

Changes Made

  • Adapter/guard reads listed above; two small module-level helpers (_yb_secret, _sig_secret) mirroring the canonical shape used across ~16 other call sites. No behavior change for single-profile deployments (covered by tests).
  • tests/gateway/test_platform_authz_scope.py — 17 regression tests driving the real scope contextvar:
    • each of the four helpers prefers a scope value over os.environ;
    • under multiplex (_MULTIPLEX_ACTIVE=True), a scoped miss returns the default and never falls through to process env (the leak direction), for all four adapters;
    • without a scope (single-profile), os.environ fallback is preserved;
    • WeixinAdapter / wecom _open_dm_opted_in and yuanbao AccessPolicy gates stay closed when only the default profile's GATEWAY_ALLOW_ALL_USERS=true is present, and open on their own scope's opt-in;
    • the yuanbao open-policy startup guard accepts a scope-installed opt-in and rejects when the scope lacks it even though os.environ has it.

How to Test

  1. uv run python -m pytest tests/gateway/test_platform_authz_scope.py -q → 17 passed.
  2. Neighbors: uv run python -m pytest tests/gateway/test_multiplex_profile_authz.py -q → 5 passed.
  3. ruff check gateway/platforms/weixin.py gateway/platforms/yuanbao.py gateway/platforms/signal.py plugins/platforms/wecom/adapter.py gateway/run.py tests/gateway/test_platform_authz_scope.py → clean.
  4. Manual (multiplex): configure profiles A+B, put WEIXIN_ALLOWED_USERS=<id> in B's .env only → B now answers allowlisted senders (previously silent); set GATEWAY_ALLOW_ALL_USERS=true in A only → B still denies strangers (previously admitted).

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run targeted test suites locally and they pass
  • I've added tests for my changes (required for bug fixes)
  • I've tested on my platform: Windows 11

Documentation & Housekeeping

Screenshots / Logs

N/A

…secret scope

Under gateway.multiplex_profiles, secondary profiles are constructed
inside _profile_runtime_scope and their .env lives in the profile's
secret scope - gateway/run.py explicitly does NOT mutate os.environ with
it. Four adapters still read their AUTHORIZATION config via raw
os.getenv, so every secondary profile either (a) silently missed its own
env-only allowlists/policies (fail-closed: all DMs dropped at intake) or
(b) inherited the default profile's GATEWAY_ALLOW_ALL_USERS=true /
allowlists from the shared process env (fail-open admissions):

- weixin.py: WEIXIN_DM_POLICY / WEIXIN_ALLOWED_USERS /
  WEIXIN_GROUP_ALLOWED_USERS / WEIXIN_ALLOW_ALL_USERS +
  GATEWAY_ALLOW_ALL_USERS in _open_dm_opted_in
- yuanbao.py: YUANBAO_DM_POLICY / DM_ALLOW_FROM / GROUP_POLICY /
  GROUP_ALLOW_FROM / ALLOW_ALL_USERS (new _yb_secret helper; AccessPolicy
  hard-gates intake)
- signal.py: SIGNAL_GROUP_ALLOWED_USERS / SIGNAL_ALLOWED_USERS (new
  _sig_secret helper; empty scoped group list previously meant "drop all
  groups" silently)
- wecom/adapter.py: WECOM_DM_POLICY / WECOM_ALLOWED_USERS /
  WECOM_GROUP_POLICY / WECOM_ALLOW_ALL_USERS + GATEWAY_ALLOW_ALL_USERS -
  while credentials one line above already used _get_scoped_secret
- gateway/run.py::_own_policy_open_startup_violation: the open-policy
  startup guard validated GATEWAY_ALLOW_ALL_USERS via raw os.getenv even
  though its sibling dm/group reads already used the scoped _getenv

All reads now go through the canonical fail-closed scoped shape QQ's
_resolve_qq_secret already used (scope hit wins; unscoped single-profile
callers keep legacy os.environ behavior). Regression suite drives the
real scope contextvar across all four helpers plus the admission gates
and the startup guard, asserting both directions: profile values are
visible under multiplex, default-profile values never leak.

Fixes NousResearch#93522
@alt-glitch alt-glitch added type/security Security vulnerability or hardening P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins platform/signal Signal CLI adapter platform/wecom WeCom / WeChat Work adapter area/auth Authentication, OAuth, credential pools sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Aug 24, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Related: #88559 repairs the shared authorization-mixin path; this PR covers remaining adapter-local admission reads and the startup check for the same multiplexed-profile isolation family.

@trevorgordon981 trevorgordon981 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed from a Hermes audit pass. Correct direction — routing authorization reads through the per-profile scope is exactly what #93522 needs, and routing it through _get_scoped_secret / _getenv is the right canonical shape. One fail-open concern inline, plus a merge-coordination note:

Cross-PR: this substantially overlaps #93545 (same issue #93522, same files) but diverges in approach — #93545 uses the canonical gateway.authz_mixin._platform_gate_env and explicitly leaves weixin/wecom _open_dm_opted_in and Signal unscoped (deferring to the 1,091-commit-stale #88559), whereas this PR fully closes those but invents per-file _sig_secret/_yb_secret helpers instead of routing through a shared one. Merging both will conflict. Worth reconciling with the other author which lands, and confirming #93545's base isn't divergent — the canonical helper it cites isn't present in the current tree.

try:
val = get_secret(name, default)
except UnscopedSecretError:
val = os.getenv(name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This except-fallback fails OPEN to os.environ. For an authorization gate, an UnscopedSecretError resolving to the global/default-profile env is the exact cross-profile leak #93522 describes. It is only safe if every secondary profile is guaranteed to construct inside _profile_runtime_scope; if that invariant ever breaks, a secondary profile silently inherits the default profile's allow-all / allowlists. Safer to fall back only when multiplex is genuinely disabled (an explicit primary-profile path) rather than on a blanket exception swallow. Same pattern applies to _yb_secret (yuanbao.py L1280) and the wecom helper.

@teknium1

Copy link
Copy Markdown
Collaborator

Merged via #93639: your commit was cherry-picked on top of #93545's base (authorship preserved) — your PR was the only one covering signal and the weixin/wecom allow-all gates. Thanks @aniruddhaadak80!

@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 comp/plugins Plugin system and bundled plugins P2 Medium — degraded but workaround exists platform/signal Signal CLI adapter platform/wecom WeCom / WeChat Work adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages 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]: Multiplexed-profile authorization reads bypass the profile secret scope (weixin / yuanbao / signal / wecom)

4 participants