← Community
bugfixed

oauth_refresh uses the unscoped _vault_get, so a module reads another module's OAuth access token without declaring it (v0.99)

ShwetaShweta#112d ago · 32 views
affected: station-v0.99fixed in: station-v1.4.0

Reproduction steps:

  1. A victim module authenticates a provider via OAuth (Salesforce/HubSpot/Google/…);

its credential — including a cached live access token — is saved at
keys.local.json["victimmod::salesforce"].

  1. Install any attacker module (trust default "any"). In its handler it calls the

injected helper:
oauth_refresh("victimmod::salesforce")
— it does NOT declare that provider, and does NOT use the "::" namespaced-key
trick of finding #24.

  1. Run repro_oauth_refresh_unscoped_cross_module_token_theft_v099.py against a

clean v0.99 extraction. Output:
scoped vault_get('victimmod::salesforce') -> None (scoping refuses it)
oauth_refresh('victimmod::salesforce') -> access_token: 00Dxx_VICTIM… (STOLEN)

Expected:
The per-module vault scoping (routes/modules.py:379) must apply to EVERY path that
hands a module a credential, including the OAuth helper — a module may refresh
only providers it declared, and may never obtain another module's tokens.

Actual:
The module namespace is given the RAW studio_server._oauth_refresh as its
oauth_refresh helper (routes/modules.py:1053) — NOT a per-module wrapper. Inside
it, the credential is resolved with the UNSCOPED _vault_get(provider)
(studio_server.py:4918 -> credential_resolver.resolve), the same function the
scoped shim exists to replace. So a module calls oauth_refresh("victimmod::salesforce")
for ANY provider name and gets back that credential's live access token (and
instance_url); on a cache hit it returns the cached token with ZERO network
round-trip. The repro shows the module's OWN scoped vault_get correctly refusing
"victimmod::salesforce" while oauth_refresh hands over the victim's token — so this
is a SECOND bypass of the Finding-1 scoping, broader than #24 (no declaration and
no namespaced-key needed; it applies to every OAuth provider). The refresh path
also writes back to the victim's slot (_vault_save at studio_server.py:4967), so a
forced refresh mutates a sibling's credential entry too.

Suggested fix:
Make oauth_refresh a PER-MODULE helper, exactly like vault_get: bind it at module
load so it resolves the credential through that module's scoped shim
(_make_module_vault_get) and can only refresh/write providers the module itself
declared. More generally, NO helper handed into a module namespace may call the
raw _vault_get / _vault_save / credential_resolver.resolve with a module-supplied
provider name — every credential path a module can reach must go through the same
per-module scope (and reserved-key / "::" filter, cf. #23/#24/#25).

5 pts

1 reply

Fixed in station-v1.4.0. oauth_refresh is now injected per-module and scoped to that module's declared providers (credential_spec / allowed_destinations / command requires); an out-of-scope, ::-namespaced, or reserved-key provider is refused before the vault is touched. This closes the second injected helper that bypassed the vault_get shim's scope.

Thanks for the report — credited.

Sign in to reply.