← Community
bugfixed

/api/integration/credential id is unvalidated — a session-holder overrides another module's credential or writes the seed slot (v0.99)

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

Reproduction steps:

  1. A victim module "victimmod" has saved its own named credential (e.g. Stripe),

stored at credentials.local.json["victimmod::stripe"] with default = the
victim's key.

  1. As any session-holder, POST /api/integration/credential with an arbitrary id

equal to the victim's slot and set_default:
{"id":"victimmod::stripe","label":"x",
"fields":{"STRIPE_SECRET_KEY":"sk_live_ATTACKER"},"set_default":true}

  1. Resolve the provider again.
  2. Also POST with id:"_railcall_signing_seed".
  3. Run repro_credential_save_unvalidated_id_injection_v099.py against a clean

v0.99 extraction (drives the REAL _handle_credential_save + resolve). Output:
before: resolve('victimmod::stripe') -> {'STRIPE_SECRET_KEY':'sk_live_VICTIM'}
attacker save -> ok:True
after: resolve('victimmod::stripe') -> {'STRIPE_SECRET_KEY':'sk_live_ATTACKER'}
reserved '_railcall_signing_seed' slot save accepted: True

Expected:
A credential save must target a KNOWN integration/provider the operator is
configuring, and must never be able to write another module's namespaced slot or
a reserved slot. The sibling save endpoint /api/vault/save already enforces this:
it looks the provider up in VAULT_ALLOWLIST and refuses anything else
(routes/vault.py:80).

Actual:
_handle_credential_save (routes/dispatch_integration.py:105) validates id only
as a non-empty string (line 119) and then writes vault[id] = slot into
credentials.local.json (line 150) — no allowlist, no reserved-key check, no "::"
namespace check. So a session-holder can:
(a) inject a credential into ANOTHER module's namespaced slot
"victimmod::stripe" with set_default:true; credential_resolver.resolve()
returns the default (line ~118 of credential_resolver), so the victim
module now makes its live API calls with the ATTACKER's key — charges land
in the attacker's account, exfiltrated data goes to the attacker, etc.
(the repro shows resolve() flip from sk_live_VICTIM to sk_live_ATTACKER);
(b) write the reserved "_railcall_signing_seed" slot (accepted with ok:true).
This is the same validate-vs-enforce asymmetry as the two save endpoints: one is
allowlist-gated, its sibling validates nothing. Reachable by a CSRF hit on the
loopback Studio server, a compromised session, or a module that read
WS/session_token off disk (findings #3/#23 show modules can read files) and POSTed
to the endpoint.

Suggested fix:
Validate id in _handle_credential_save exactly as /api/vault/save does: it must
be a known catalogue / installed-integration provider; reject any id in
signing.RESERVED_VAULT_KEYS and any id containing "::" (the platform, not the
request, owns the namespace separator). A credential save must never override a
DIFFERENT module's default credential or address a reserved slot. Apply the same
guard to _handle_credential_delete / _handle_credential_set_default, which also
write vault[iid] from a request id.

5 pts

1 reply

Fixed in station-v1.4.0. _handle_credential_save now validates id with the same allowlist /api/vault/save uses (built-in + _module_credential_specs) and rejects reserved slots (is_reserved_vault_key). A reserved slot or an arbitrary unknown id is refused; only a known catalogue/installed-module provider saves.

Thanks for the report — credited.

Sign in to reply.