CVE-2026-57518: Verified Repro With Script Download
CVE-2026-57518: Pagekit CMS privilege escalation leading to RCE
CVE-2026-57518 is verified against pagekit/pagekit · github. Affected versions: <= 1.0.18. Fixed in None available; project archived. Vulnerability class: RCE. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00256.
What Is CVE-2026-57518?
CVE-2026-57518 is a high-severity privilege escalation vulnerability in Pagekit CMS 1.0.18 and earlier that lets an authenticated low-privileged user escalate to admin-level permissions and ultimately achieve remote code execution. Pruva reproduced it (reproduction REPRO-2026-00256).
CVE-2026-57518 Severity & CVSS Score
CVE-2026-57518 is rated high severity, with a CVSS base score of 8.8 out of 10.
High — serious impact or readily exploitable. Prioritize remediation.
Affected pagekit/pagekit Versions
pagekit/pagekit · github versions <= 1.0.18 are affected.
How to Reproduce CVE-2026-57518
pruva-verify REPRO-2026-00256 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00256/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-57518
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
authenticated low-privileged user with 'user: manage users' permission self-assigns a custom role with 'system: manage packages' via POST /api/user/{id}, then uploads and installs a malicious PHP package
- POST /api/user/{id} with roles array containing custom role ID (not 3)
- POST /admin/system/package/upload
- POST /admin/system/package/install
- GET /packages/{name}/index.php?cmd=id
Distinct alternate RCE sink: the same UserApiController::saveAction() privilege escalation (only blocks Administrator role id=3) grants the trusted 'system: software updates' permission instead of 'system: manage packages', unlocking a SECOND independent RCE sink — UpdateController::downloadAction + updateAction drive…
How the agent worked
Root Cause and Exploit Chain for CVE-2026-57518
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.
- 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 userscan 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:
$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
- Script:
bundle/repro/reproduction_steps.sh - 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-apachewith 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
- Package Manager (ID 4):
- As administrator, creates a low-privileged editor user (ID 2) with the User Manager role.
- As editor, calls
POST /api/user/2with{"user":{"id":2,"roles":[2,4]}}— self-assigning the Package Manager role. ThesaveActiononly 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/uploadand installs it viaPOST /admin/system/package/install. - Accesses
/packages/pagekit/shell/index.php?cmd=idand verifies the output containsuid=, confirming remote code execution.
- Downloads (or reuses a cached copy of) the official Pagekit 1.0.18
release ZIP (which includes all PHP dependencies in
- Expected evidence of reproduction:
bundle/logs/reproduction_steps.log— full execution logbundle/logs/exploit_evidence.txt— structured evidence summarybundle/logs/rce_id_output.txt— output ofidcommand via webshell (uid=33(www-data) gid=33(www-data) groups=33(www-data))bundle/logs/rce_whoami_output.txt— output ofwhoami(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)
- 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.
- 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). - Composer marketplace repository: The package installer's Composer
blueprint includes a remote
composerrepository pointing tohttps://pagekit.com. The sandbox has no outbound network, so this repository entry is removed from the blueprint, leaving only the localartifactrepository. 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 userscannot self-assign a custom role withtrustedpermissions. - 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 usersprecondition 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.
Variant Analysis & Alternative Triggers for CVE-2026-57518
A distinct alternate RCE sink was found and confirmed at runtime. The parent
CVE-2026-57518 demonstrates that an authenticated low-privileged user holding
user: manage users can self-assign an arbitrary custom role via
UserApiController::saveAction() (which only blocks the built-in Administrator
role id=3) and then RCE through the package installer
(POST /admin/system/package/upload|install, gated by
system: manage packages). This variant shows that the same saveAction
escalation grants a different trusted permission —
system: software updates — which unlocks a second, independent RCE sink:
UpdateController::downloadAction + updateAction
(POST /admin/system/update/download + POST /admin/system/update/update).
downloadAction fetches an attacker-controlled URL (a file:// URL copies a
locally planted archive with no network needed) into a server-side temp file,
and updateAction runs SelfUpdater::update(), which extracts the archive over
the Pagekit root (App::path()). A webshell placed at the archive root is
written to /var/www/html/<name>.php and executed directly by Apache. RCE as
www-data was confirmed (uid=33(www-data)). This is an alternate trigger /
sink, not a bypass of a fix — no patched version exists (project archived).
Fix Coverage / Assumptions
- Invariant the (would-be) fix relies on: the
saveActioncheck assumes that only the built-in Administrator role (id=3) is privileged, and that blocking add/remove of id=3 for non-administrators is sufficient to prevent privilege escalation. - Code path it explicitly covers: adding or removing
Role::ROLE_ADMINISTRATOR(id=3) on any target user by a non-administrator caller. - What it does NOT cover: (a) assignment of any custom role (id ≥ 4),
including roles carrying
trustedpermissions such assystem: manage packages,system: software updates,user: manage user permissions,system: access system settings; (b) thetrustedpermission flag, which is advisory UI-only and never enforced server-side; (c) the second RCE sink (UpdateController/SelfUpdater) that becomes reachable oncesystem: software updatesis obtained — the parent reproduction only exercises the package-installer sink.
Variant / Alternate Trigger
Entry points (distinct from the parent):
POST /api/user/{id}—UserApiController::saveAction()(app/system/modules/user/src/Controller/UserApiController.php, lines ~178–183). Same escalation root cause as the parent, but the self-assigned custom role carriessystem: software updates+system: access admin areainstead ofsystem: manage packages.POST /admin/system/update/download—UpdateController::downloadAction()(app/installer/src/Controller/UpdateController.php). Attacker-controlledurlparameter;file_put_contents(tempnam(path.temp,'update_'), fopen($url,'r')). The server copies the crafted "update" ZIP into a temp file whose path is stored in the session assystem.update.POST /admin/system/update/update—UpdateController::updateAction(). Readssystem.updatefrom the session and invokesSelfUpdater::update($file)(app/installer/src/SelfUpdater.php), which extracts the ZIP overApp::path()(the Pagekit web root).GET /rce_variant.php?cmd=id— the extracted webshell, served directly by Apache (root.htaccessonly rewrites non-existent paths:RewriteCond %{REQUEST_FILENAME} !-f).
Why this is materially distinct from the parent (not a relabel):
| Dimension | Parent (CVE-2026-57518) | This variant |
|---|---|---|
| Admin endpoint | /admin/system/package |
/admin/system/update |
| Required permission | system: manage packages |
system: software updates |
| Payload format | pagekit-extension ZIP with composer.json |
"self-update" ZIP with app/installer/requirements.php |
| Sink primitive | PackageManager extracts into packages/ |
SelfUpdater::extract() overwrites core files at web root |
| Network need | local upload (multipart) | file:// URL → server-side copy (no outbound network) |
The escalation root cause is shared (the saveAction id=3-only check),
which is exactly why a complete fix must block self-assignment of all
trusted/elevated permissions and treat both admin sinks as exposed.
- Package/component affected:
pagekit/pagekit- Escalation:
app/system/modules/user/src/Controller/UserApiController.php(saveAction) - Variant sink:
app/installer/src/Controller/UpdateController.php(downloadAction,updateAction) andapp/installer/src/SelfUpdater.php(update,extract)
- Escalation:
- Affected versions (as tested): Pagekit 1.0.18, commit
95446c5f08fc43993fad5e2b581743ffcce50c35(only tag / latest commit; project archived 2023-12-01; no patched version exists). - Risk level: High (CVSS 8.8 — same as parent: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H).
- Consequences: Full server compromise as the web server user (
www-data). The self-updater sink is arguably more powerful than the package-installer sink: it can overwrite any file under the Pagekit root (not justpackages/), limited only bywww-datawrite permissions and thepackages//storage//`.htaccess ignore list.
Impact Parity
- Disclosed/claimed maximum impact (parent): Privilege escalation leading to
remote code execution (
code_execution). - Reproduced impact from this variant run: Full privilege escalation to
system: software updates, followed byUpdateControllerdownload + update drivingSelfUpdaterto extract an attacker webshell to the web root, followed by confirmed RCE (uid=33(www-data) gid=33(www-data) groups=33(www-data),whoami=www-data). - Parity:
full. - Not demonstrated: Root access (RCE is as
www-data, same as the parent). Persistence/defense-evasion beyond a single webshell was not attempted.
Root Cause
UserApiController::saveAction() authorizes role changes by searching the
submitted roles array for only Role::ROLE_ADMINISTRATOR (constant 3):
$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.');
}
Because $add/$remove are derived solely from the presence/absence of id=3,
any custom role (id ≥ 4) produces $add = $remove = false, so the guard never
fires for a non-administrator caller. The submitted roles are then persisted
unchanged via $user->save($data). A custom role created by an administrator
that carries system: software updates (a trusted permission that is
advisory-only server-side) can therefore be self-assigned by any user with
user: manage users. Once held, UpdateController — gated only by
@Access("system: software updates", admin=true) — exposes an arbitrary-URL
download and a SelfUpdater that extracts the downloaded archive over the web
root, yielding RCE.
Fix commit: None. Pagekit 1.0.18 is the final release; the repository is archived.
Reproduction Steps
- Script:
bundle/vuln_variant/reproduction_steps.sh - What the script does:
- Reuses the cached Pagekit 1.0.18 release and the
pagekit-vuln-1018:reproDocker image (php:7.4-apache+ SQLite), starts an isolated containerpagekit-variant, and installs Pagekit viaPOST /installer/install. - As administrator, creates an Updater role (id=4) with
system: software updates+system: access admin area, a User Manager role (id=5) withuser: manage users, and a low-privileged editor user (id=2) holding only the User Manager role. - As editor, calls
POST /api/user/2withroles=[2,4]— self-assigning the Updater role.saveActiononly blocks id=3, so id=4 passes through unchecked. Escalation confirmed (roles become[2,4]). - Crafts a malicious "self-update" ZIP (
app/installer/requirements.phpreturning an emptygetFailedRequirements()+ a root-level webshellrce_variant.php) and plants it at/var/www/html/storage/evil_update.zip. - As the escalated editor, calls
POST /admin/system/update/downloadwithurl=file:///var/www/html/storage/evil_update.zip(HTTP 200), thenPOST /admin/system/update/update(HTTP 200;SelfUpdaterruns: "Extracting files...done", "Removing old files...done"). - Accesses
GET /rce_variant.php?cmd=idand verifiesuid=33(www-data). - Performs the mandatory fixed-version check (none exists) and writes the runtime manifest + evidence.
- Reuses the cached Pagekit 1.0.18 release and the
- Expected evidence of reproduction:
bundle/logs/vuln_variant/reproduction_steps.log— full execution logbundle/logs/vuln_variant/exploit_evidence.txt— structured evidencebundle/logs/vuln_variant/rce_id_output.txt—uid=33(www-data) gid=33(www-data) groups=33(www-data)bundle/logs/vuln_variant/rce_whoami_output.txt—www-databundle/logs/vuln_variant/fixed_version.txt— no-fixed-version noticebundle/vuln_variant/runtime_manifest.json— runtime evidence manifest
Evidence
Key excerpts
Privilege escalation (Step 3):
Editor BEFORE escalation: ...,"roles":[2,5],...
Escalation response: {"message":"success","user":{"id":2,...,"roles":[2,4],...}}
PRIVILEGE ESCALATION CONFIRMED — editor now has 'system: software updates'
Variant sink (Steps 5–6):
Download response: [] HTTP:200 (POST /admin/system/update/download, file:// URL)
Update response (tail): Removing old files...done. Deactivating update mode...done. HTTP:200
Webshell on disk (/var/www/html/rce_variant.php): yes
RCE (Step 7):
RCE output (id): uid=33(www-data) gid=33(www-data) groups=33(www-data)
RCE output (whoami): www-data
=== VARIANT 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; commit
95446c5f08fc43993fad5e2b581743ffcce50c35) - Web server: Apache 2.4.54 with mod_rewrite
Sandbox adaptations (non-security-relevant)
- Docker networking: all HTTP requests to the running service are made via
docker exec(curl inside the container); this does not alter request handling. - Session bootstrap: a PHP helper creates a valid authenticated session directly in the database (same approach as the parent repro, to bypass a PHP 7.4 / Symfony 3.0 session-storage interaction that prevents curl login). The resulting auth state is identical to a successful web login.
file://download URL: the variant usesfile://fordownloadAction's$urlso the server-side copy works without outbound network. This is strictly weaker than the real-world attack (where an attacker can host the archive on any HTTP server they control and pass anhttp://URL); it does not change any security-relevant code path.
Recommendations / Next Steps
- Fix
saveActionto validate every role, not just id=3. A non-administrator caller must be forbidden from assigning any role whose permissions aretrusted/elevated, or (more conservatively) any role the caller does not already hold / any role granting a permission the caller lacks. Apply the same logic inbulkSaveAction(which delegates tosaveAction). - Enforce the
trustedpermission flag server-side. Today it is UI-only. The role-editor (RoleApiController) and the user-role assignment (UserApiController) should reject grantingtrustedpermissions to roles that non-administrators can hold, and reject assigning such roles to non-administrator users. - Harden
UpdateController::downloadAction: validate/restrict theurlscheme and host (nofile://, no SSRF to internal hosts), and require signature/verification of update archives beforeSelfUpdater::extract()overwrites core files. Treat the self-updater as a sensitive RCE-equivalent sink equal to the package installer. - Defense in depth: restrict
SelfUpdater::extract()to a manifest of expected paths and refuse to write executable files outside an allow-list; do not blindly$zip->extractTo(App::path(), $fileList).
Additional Notes
Bypass vs. alternate trigger
This is an alternate trigger/sink, not a bypass of a released fix — no patched version exists (project archived 2023-12-01; 1.0.18 is the only tag and latest commit). The variant is confirmed on the only available (vulnerable) target and demonstrates that a complete remediation must cover both the package-installer and self-updater sinks and all trusted permissions.
Idempotency confirmation
bundle/vuln_variant/reproduction_steps.sh was executed three times
consecutively. Every run produced the same result: privilege escalation to
system: software updates confirmed, UpdateController download + update
succeeded (HTTP 200), webshell written to the web root, and RCE confirmed
(uid=33(www-data)). Each run starts a fresh container with a clean Pagekit
installation, so no state carries over. Exit code 0 on every run.
Limitations
- RCE executes as
www-data(Apache user), not root — same as the parent. - The
SelfUpdatercleanup step deletesapp/files absent from the update archive, which breaks the Pagekit application after extraction; the standalone root-level webshell (rce_variant.php) remains independently executable by Apache regardless, so RCE is still confirmed. A real attacker would include the fullapp/tree in the archive to preserve a functional site. - The best-effort session-path probe (
check_update_path.php) reported<not found>because Pagekit's exact session serialization layout for thesystem.updatevalue differs from the naive regex; this is cosmetic only — the update step itself returned HTTP 200 with theSelfUpdater's "done" output and the webshell was verified on disk, which is the authoritative confirmation.
CVE-2026-57518 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-57518
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-57518
Upgrade pagekit/pagekit · github to None available; project archived or later.
FAQ: CVE-2026-57518
How does the CVE-2026-57518 escalation-to-RCE chain work?
user: manage users self-assigns a custom role (ID >= 4) that carries system: manage packages, then uses the resulting admin package-installer functionality to upload and install a malicious PHP extension package containing a web shell, achieving remote code execution as the web server user.Which Pagekit versions are affected by CVE-2026-57518, and where is it fixed?
How severe is CVE-2026-57518?
How can I reproduce CVE-2026-57518?
user: manage users, self-assigns a custom role granting system: manage packages, then uploads a malicious PHP package via the admin installer to confirm code execution as www-data.References for CVE-2026-57518
Authoritative sources for CVE-2026-57518 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.