> ## Content Index
> Fetch the complete content index at: https://plaintext.block8.ai/llms.txt
> Use this file to discover other available public pages before exploring further.

# When "Save Page" Means "Save Anywhere You Like" (CVE-2026-78752)
- URL: https://plaintext.block8.ai/blog/when-save-page-means-save-anywhere-you-like-cve-2026-78752/
- Published: 2026-10-21T23:55:00.000Z
- Updated: 2026-10-21T23:54:59.000Z
- Author: Lukasz S

## TL;DR

Pluck CMS 4.7.20 and earlier lets an authenticated administrator write PHP files anywhere the web server can reach, not just into the pages folder where they belong. The page editor's `sub_page` field is read straight from `$_POST`, never sanitised, and concatenated into a file path. Feed it a traversal sequence like `../../../` and the saved file lands in the webroot instead of the pages directory. That's a High-severity path traversal (CVSS 7.2, CWE-22) that gives an attacker a persistent, partly attacker-controlled file on a web-accessible path.

## What the page editor is supposed to do

Pluck is a flat-file CMS, which means pages aren't rows in a database. They're actual files on disk. When you edit a page in the admin panel and hit Save, Pluck writes a `.php` file into its pages directory. That's the whole design, and it's a perfectly reasonable one for a lightweight CMS.

The code that does this lives in `save_page()` inside `data/inc/functions.admin.php`. It builds a destination path by gluing together the pages directory constant and the details of the page you're saving:

```php
save_file(PAGE_DIR.'/'.$subpage.$number.'.'.$seo_title.'.php', $data);

```

The quiet assumption here is that `$subpage` is a tame little string: a sub-page name, nothing more. If it were, the file would always land inside `PAGE_DIR`, exactly where it should.

## Where the trust breaks

`$subpage` comes directly from the `sub_page` POST parameter, and nobody checks what's in it.

You might expect Pluck's security module to catch this. It has one: `data/inc/security.php`. But here's the gap that makes the whole thing work. That module filters `$_GET` values. It does not touch `$_POST`. The `sub_page` parameter arrives via POST, so it sails straight past the guard untouched.

So the attacker-supplied string gets concatenated right into the path handed to `save_file()`. Put `../../../` in there and the resolved destination climbs out of the pages directory and up into the webroot, or anywhere else the `www-data` process can write.

## Under the bonnet

The attack needs admin credentials, which is why this is rated PR:H. With a valid admin session, the request is almost mundane:

```http
POST /admin.php?action=editpage HTTP/1.1
Host: <target>
Cookie: PHPSESSID=<admin_session>
Content-Type: application/x-www-form-urlencoded
Referer: http://<target>/admin.php

title=poc&seo_name=poc&content=BLOCK8_CVE_POC&sub_page=../../../&hidden=no&save=Save

```

With `sub_page=../../../`, the file (named something like `1.poc.php`) is created in the webroot rather than the pages folder. You can then fetch it directly:

```bash
curl http://<target>/1.poc.php

```

The response is an HTTP 200 serving the PHP file, which contains the marker `BLOCK8_CVE_POC` drawn from the page content. The page title, SEO name, and body are all partly attacker-controlled, so the written file's contents aren't arbitrary, but they're far from fixed either.

## Why it matters

This is rated CVSS 7.2 (High) with the vector `AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H`, classified as CWE-22, Improper Limitation of a Pathname to a Restricted Directory. High confidentiality, integrity, and availability impact, gated behind needing an admin account.

The write primitive on its own is already serious. An attacker can drop or overwrite web-accessible PHP files, and can corrupt CMS configuration, for example by clobbering `data/settings/pass.php` to break authentication for everyone. That's persistence and sabotage in one move.

Full, clean arbitrary code execution does take one more step: the written file's content is only partly attacker-controlled, so turning it into a reliable shell means chaining with the CSRF bypass that also ships in 4.7.20 (where `requestedByTheSameDomain()` returns `true` whenever the `HTTP_REFERER` header is simply absent). We're calling that honestly. The traversal write is demonstrated and dangerous by itself; the clean-RCE escalation is a known chain, not a hypothetical hand-wave.

## How to fix it

The fix is to stop trusting `sub_page` and to stop filtering only half of your input sources:

1. **Sanitise `sub_page` before it touches a path.** Strip or reject traversal sequences, and ideally restrict the value to a known-safe character set (letters, numbers, hyphens). Resolve the final path and confirm it still sits inside `PAGE_DIR` before writing.
2. **Filter POST, not just GET.** The security module guarding `$_GET` while leaving `$_POST` wide open is the structural flaw underneath this bug. Any input that reaches a filesystem path, a query, or an include needs the same scrutiny regardless of HTTP method.
3. **Run the web process with least privilege.** If `www-data` can't write to the webroot, the blast radius of a traversal write shrinks considerably.

## Closing thought

A flat-file CMS lives and dies by how carefully it turns user input into file paths. The Block8.ai Security Research Team found that Pluck's page editor trusted a POST field to stay inside its lane, and a security module that only watched one door while the other stood open. Put those two together and "Save Page" quietly becomes "Save anywhere `www-data` can reach".