Add File Manager sub-app (Pro feature) (v2.7.0)

New browse/edit interface for the WordPress install's own files, built
using the installed "WP File Manager" plugin (elFinder-based) as a
functional reference rather than embedding it. Own REST controller
(FileManagerData.php) + Vue page, same pattern as every other sub-app.

Security is the whole point here (highest-risk surface in the plugin —
arbitrary filesystem read/write):
- Root is ABSPATH, manage_options-gated throughout.
- Every client path is resolved via realpath() and checked for root
  containment against the *resolved* path, not string-matched against
  the raw input — catches symlink tricks and encoded traversal that a
  naive "../" check would miss.
- Uploads/new files/renames reject a blocklist of server-executable
  extensions (.php, .cgi, .sh, etc.) — the single most important
  control, since uploading a webshell is the classic exploit path for
  this class of feature.
- Deliberately not built: zip/unzip (zip-slip vulnerability class),
  chmod/permissions editing, cross-directory move/copy.

Gated as a paid feature (riskier than Database Explorer, which is
already paid) — a judgment call flagged to the user, not something they
specified.

Also fixed a real bug found while wiring this up: SubApps.php's
page-level lock doesn't stop someone with manage_options from calling
the REST endpoints directly, bypassing the license check entirely.
File Manager's REST permission callbacks now also check
License::is_feature_available(). The same gap exists in Database
Explorer, Role Editor, and Activity Log's REST controllers and is
flagged as a follow-up task, not fixed here (out of scope for what was
asked, affects three unrelated features).

Verified live via direct REST calls: path traversal blocked (404),
blocked extensions rejected (400), root deletion blocked (400), and a
full create/write/read/rename/delete round trip succeeded cleanly.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
nixpanel
2026-07-05 06:59:24 -05:00
co-authored by Claude Sonnet 5
parent 204c4365e2
commit 15d6060ae3
12 changed files with 1228 additions and 4 deletions
+63 -1
View File
@@ -1,6 +1,6 @@
# Aurora Admin v2 — Project Status
_Last updated: 2026-07-05 (latest session — Pages/Media/Users replacement pages, completing the "skinned vs. replaced" conversion started with Posts)_
_Last updated: 2026-07-05 (latest session — new File Manager sub-app; found a license-bypass gap affecting several paid REST controllers)_
## What this project is
@@ -702,6 +702,68 @@ compatibility, trust), which wasn't in scope here.
artifacts (the mu-plugin, the tampered file, the test manifest) were
removed from the LocalWP install afterward.
## File Manager (new sub-app, Pro feature)
Added this session — the user has the "WP File Manager" plugin (elFinder-
based) installed on the test site and asked for equivalent functionality
built into Aurora, as a fresh implementation rather than embedding
elFinder. `admin/src/FileManager/FileManagerData.php` (REST) +
`app/src/pages/file-manager/FileManager.vue` (entry `file-manager.js`),
registered as `aurora-admin-file-manager` in `SubApps.php` (parented under
`aurora-admin` like Menu Creator/Role Editor, not a native-page
replacement — there's no native WP file browser to redirect from).
- **Root is `ABSPATH`** (the WordPress install directory), same as the
reference plugin. Capability is `manage_options` everywhere, no
exceptions — by far the highest-risk surface in the plugin (arbitrary
filesystem read/write on the site's own PHP).
- **Path containment**: every client-supplied path goes through
`resolve_path()`, which `realpath()`s the candidate and checks the
*resolved* path is still inside the root — never a raw string check
against `"../"`, since that alone doesn't catch symlinks or encoded
traversal. New files/folders (which don't exist yet, so `realpath()`
would return false) resolve their *parent* directory instead, then just
sanitize the new name component (`wp_basename()`, reject empty/`.`/`..`/
control characters).
- Verified live via direct REST calls: `?path=../../../etc` → 404
(correctly rejected); creating/renaming to a blocked extension → 400;
deleting the root path itself → 400; a full create-folder → create-
file → write → read → rename → recursive-delete round trip all
succeeded and left no trace afterward.
- **Blocked extensions on upload/create/rename**
(`FileManagerData::BLOCKED_EXTENSIONS`): `.php` and friends, `.cgi`,
`.sh`, `.exe`, etc. — the single most important control, since
uploading a webshell through a "file manager" is the classic way this
class of feature gets exploited. A separate `BINARY_EXTENSIONS` list
(images/archives/media/fonts) plus a 2MB size cap keeps the text editor
from trying to open something it shouldn't.
- **Deliberately not built** (scope decisions): zip/unzip (extraction has
its own well-known vulnerability class — zip-slip, where a crafted
archive's entries escape the extraction directory), chmod/permissions
editing (too easy to accidentally lock yourself out or open a real
hole), cross-directory move/copy (rename within the same directory is
supported; a full move/copy tree is meaningfully more surface for a v1).
- **Gated as a paid feature** (`License::PAID_FEATURES["file-manager"]`)
— this was a judgment call, not something the user specified; reasoned
as "even riskier than Database Explorer, which is already paid," and
flagged to the user as a default they could override. They didn't
object.
- **Real bug found and fixed while wiring this up, not specific to File
Manager**: `SubApps.php`'s page-level lock (`render()` swaps in the
upgrade prompt) only gates the *page* — it doesn't stop someone with
`manage_options` from calling the REST endpoints directly and getting
full functionality regardless of license status. File Manager's REST
permission callbacks were fixed to also check
`License::is_feature_available('file-manager')`. **Database Explorer,
Role Editor, and Activity Log's REST controllers have the exact same
gap and are NOT yet fixed** — flagged as a follow-up task
(`task_cb1ac5ca` in this session) rather than fixed inline, since it's
a pre-existing issue affecting three other features, out of scope for
what was asked. If picking this back up: the fix is exactly what
`FileManagerData.php` now does — add the license check to each
`permission_callback`, matching the corresponding feature id in
`SubApps.php`'s `PAGES` const.
## Key bugs fixed (root causes, in case they resurface)
- **Admin Notices invisible on the Dashboard**: `admin_notices` prints