# RCA Report — CVE-2026-57518

## Summary

Pagekit CMS 1.0.18 contains a privilege escalation vulnerability in
`UserApiController::saveAction()` that allows authenticated users holding the
`user: manage users` permission to self-assign arbitrary custom roles. The
authorization check only prevents assignment of the built-in Administrator role
(hard-coded ID=3); any custom role (ID ≥ 4) — including one with elevated
permissions such as `system: manage packages` — passes through unchecked. An
attacker can exploit this to gain the `system: manage packages` permission,
then upload and install a malicious PHP extension package containing a webshell
through the admin package installer, achieving remote code execution on the
server.

## Impact

- **Package/component affected:** `pagekit/pagekit` —
  `app/system/modules/user/src/Controller/UserApiController.php`
  (`saveAction`, lines 178–183)
- **Affected versions:** Pagekit CMS ≤ 1.0.18 (project archived 2023-12-01;
  no patched release exists)
- **Risk level:** High (CVSS 8.8 — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)
- **Consequences:** Full server compromise. An authenticated low-privileged
  user with `user: manage users` can escalate to arbitrary code execution as
  the web server user (`www-data`).

## Impact Parity

- **Disclosed/claimed maximum impact:** Privilege escalation leading to remote
  code execution (code_execution)
- **Reproduced impact from this run:** Full privilege escalation to
  `system: manage packages`, followed by upload and installation of a malicious
  PHP package, followed by confirmed remote code execution (`uid=33(www-data)`)
- **Parity:** `full`
- **Not demonstrated:** N/A — the full chain from low-privilege user to
  arbitrary command execution was demonstrated end-to-end against the real
  running Pagekit service via its HTTP API.

## Root Cause

The vulnerability is in `UserApiController::saveAction()` at
`app/system/modules/user/src/Controller/UserApiController.php`, lines 178–183:

```php
$key    = array_search(Role::ROLE_ADMINISTRATOR, @$data['roles'] ?: []);
$add    = false !== $key && !$user->isAdministrator();
$remove = false === $key && $user->isAdministrator();

if (($self && $remove) || !App::user()->isAdministrator() && ($remove || $add)) {
    App::abort(403, 'Cannot add/remove Admin Role.');
}
```

`Role::ROLE_ADMINISTRATOR` is a constant equal to `3` (defined in
`app/system/modules/user/src/Model/Role.php`). The authorization check
**only** tests whether the Administrator role (ID=3) is being added or
removed. Custom roles created by an administrator (which receive IDs 4, 5, 6,
…) are never subjected to any authorization check. Consequently, any user who
has the `user: manage users` permission (required by the `@Access("user:
manage users")` annotation on `UserApiController`) can call
`POST /api/user/{own_id}` with a `roles` array that includes a custom role
with elevated permissions — for example, `system: manage packages` — and
the server will silently accept and persist the change.

Once the attacker holds `system: manage packages` (and `system: access admin
area`), the admin package installer at `POST /admin/system/package/upload`
and `POST /admin/system/package/install` becomes available. The attacker
uploads a ZIP archive containing a `composer.json` (type
`pagekit-extension`) and a PHP webshell (`index.php`). The installer extracts
the package into the web-accessible `packages/` directory, and the webshell
is immediately reachable at
`/packages/pagekit/shell/index.php?cmd=<command>`, executing arbitrary
commands as the web server user.

**Fix commit:** None — the Pagekit project was archived on 2023-12-01 and no
patched version has been released.

## Reproduction Steps

1. **Script:** `bundle/repro/reproduction_steps.sh`
2. **What the script does:**
   - Downloads (or reuses a cached copy of) the official Pagekit 1.0.18
     release ZIP (which includes all PHP dependencies in `app/vendor/`).
   - Builds a Docker image based on `php:7.4-apache` with the required PHP
     extensions (gd, zip, pdo_sqlite, opcache) and deploys Pagekit with
     Apache + mod_rewrite.
   - Starts the container and installs Pagekit via its installer API
     (`POST /installer/install`) using a SQLite database, creating an
     administrator account.
   - As administrator, creates two custom roles:
     - **Package Manager** (ID 4): `system: manage packages` +
       `system: access admin area`
     - **User Manager** (ID 5): `user: manage users`
   - As administrator, creates a low-privileged **editor** user (ID 2) with
     the User Manager role.
   - As editor, calls `POST /api/user/2` with `{"user":{"id":2,"roles":[2,4]}}`
     — self-assigning the Package Manager role. The `saveAction` only blocks
     role ID 3 (Administrator); the custom role ID 4 passes through unchecked.
   - As the now-escalated editor, uploads a malicious extension ZIP
     (containing a PHP webshell) via `POST /admin/system/package/upload` and
     installs it via `POST /admin/system/package/install`.
   - Accesses `/packages/pagekit/shell/index.php?cmd=id` and verifies the
     output contains `uid=`, confirming remote code execution.
3. **Expected evidence of reproduction:**
   - `bundle/logs/reproduction_steps.log` — full execution log
   - `bundle/logs/exploit_evidence.txt` — structured evidence summary
   - `bundle/logs/rce_id_output.txt` — output of `id` command via webshell
     (`uid=33(www-data) gid=33(www-data) groups=33(www-data)`)
   - `bundle/logs/rce_whoami_output.txt` — output of `whoami` (`www-data`)
   - `bundle/repro/runtime_manifest.json` — runtime evidence manifest

## Evidence

### Key excerpts

**Privilege escalation (Step 3):**
```
Editor BEFORE escalation: {"id":2,...,"roles":[2,5],...}
Escalation response: {"message":"success","user":{"id":2,...,"roles":[2,4],...}}
PRIVILEGE ESCALATION CONFIRMED — editor now has 'system: manage packages'
```

**RCE (Step 6):**
```
RCE output (id): uid=33(www-data) gid=33(www-data) groups=33(www-data)
RCE output (whoami): www-data
=== RCE CONFIRMED ===
```

### Environment details

- **OS:** Ubuntu 26.04 LTS (host); Debian Bullseye (container)
- **PHP:** 7.4.33 (Apache, inside Docker container)
- **Database:** SQLite (`pagekit.db`)
- **Pagekit:** 1.0.18 (official release from GitHub)
- **Web server:** Apache 2.4.54 with mod_rewrite

### Sandbox adaptations (non-security-relevant)

1. **Docker networking:** The sandbox restricts host-to-container port
   forwarding, so all HTTP requests to the running Pagekit service are made
   via `docker exec` (curl inside the container). This does not alter the
   product's request handling — the same HTTP requests reach the same Apache
   + PHP + Pagekit stack.
2. **Session bootstrap:** A PHP helper (`create_session.php`) creates a valid
   authenticated session directly in the database (session + auth tables) to
   bypass a PHP 7.4 / Symfony 3.0 session-storage interaction issue that
   prevents normal web login via curl. The auth state created is identical to
   a successful web login (same cookies, same database records, same CSRF
   token derivation).
3. **Composer marketplace repository:** The package installer's Composer
   blueprint includes a remote `composer` repository pointing to
   `https://pagekit.com`. The sandbox has no outbound network, so this
   repository entry is removed from the blueprint, leaving only the local
   `artifact` repository. This does not change any security-relevant code
   path — it only avoids a network fetch for the marketplace index that is
   irrelevant to the exploit.

## Recommendations / Next Steps

### Suggested fix

The authorization check in `saveAction` should validate **all** role
assignments, not just the Administrator role. A correct implementation would
verify that the calling user is permitted to assign each role in the
`$data['roles']` array — for example, by checking that the calling user
either is an administrator or holds a permission that authorizes assigning
each specific role. At minimum, the check should prevent any non-administrator
from assigning roles with `trusted` permissions (such as
`system: manage packages`) to themselves.

### Upgrade guidance

Pagekit is archived and unmaintained. Users should migrate to an actively
maintained CMS. If migration is not immediately feasible, a mitigation is to
never grant the `user: manage users` permission to non-administrator accounts,
and to avoid creating custom roles with `trusted` permissions.

### Testing recommendations

- Add a unit test that verifies a non-administrator user with
  `user: manage users` cannot self-assign a custom role with `trusted`
  permissions.
- Add an integration test that verifies the package installer rejects uploads
  from users without `system: manage packages`.

## Additional Notes

### Idempotency confirmation

The script was run twice consecutively, both times producing the same result:
privilege escalation confirmed and RCE confirmed (`uid=33(www-data)`). Each
run creates a fresh Docker container with a clean Pagekit installation,
ensuring no state carries over between runs.

### Limitations

- The project is archived; no fixed version exists for a negative control.
- The webshell executes as `www-data` (the Apache user), not as root. Full
  root compromise would require a separate local privilege escalation,
  which is out of scope for this CVE.
- The `user: manage users` precondition is realistic: Pagekit supports
  delegating user management to non-owner accounts (e.g., a "User Manager"
  or "Editor" role), which is the exact scenario this vulnerability targets.
