GHSA-245j-xjvr-xvm5 CI4MS Fileeditor allows deletion and rename of critical application files due to missing extension allowlist on destructive operations
SummaryThe Fileeditor module enforces an extension allowlist (`['css','js','html','txt','json','sql','md']`) on content-write operations (`saveFile`, `createFile`), but two destructive endpoints — `deleteFileOrFolder` and `renameFile` — never validate the extension of the *source* path. A backend user with file-editor permissions can therefore unlink or rename any file inside the project root that is not explicitly listed in the small `$hiddenItems` blocklist. Critical framework files such as `app/Config/Routes.php`, `app/Config/App.php`, `app/Config/Database.php`, `app/Config/Filters.php`, `public/index.php`, and `public/.htaccess` all live outside that blocklist and can be destroyed, producing a persistent denial of service that requires filesystem-level redeployment to recover.DetailsRoot cause: inconsistent application of the extension allowlist across Fileeditor operations in `modules/Fileeditor/Controllers/Fileeditor.php`.The class declares an allowlist used by content-write operations:`saveFile` (line 110) and `createFile` (line 167) correctly call `allowedFileTypes()` against the target path before writing. The two destructive endpoints do not:The validation gauntlet a path traverses before reaching `unlink()`/`rename()`:**Regex** `/^[a-zA-Z0-9_ \-\.\/]+$/` — admits any path made of alphanumerics, dots, dashes, underscores, slashes (matches `app/Config/Routes.php` trivially).**`isHiddenPath()`** — only blocks paths whose individual segments equal an entry in `$hiddenItems`:Critical CodeIgniter 4 framework files (`app`, `Config`, `Routes.php`, `App.php`, `Database.php`, `Filters.php`, `public`, `index.php`, `.htaccess`) are **not** members of this list, so they pass.**`realpath` + `strpos` containment** — confirms the resolved path is inside `ROOTPATH`. Routes.php, etc., are inside ROOTPATH and pass.**Sink** — `unlink()` or `rename()` runs unconditionally; no extension allowlist applied.The recent security patch in commit `379ebb6` ("Security: patch critical vulnerabilities and bump to v0.31.4.0") added `isHiddenPath()` invocations to every endpoint, addressing the previous `.env` reachability. It did **not** address the missing extension allowlist on delete and rename source paths. The inconsistency therefore survives in HEAD (v0.31.8.0).Authorization is provided by the `backendGuard` filter (`modules/Fileeditor/Config/FileeditorConfig.php:12-17`) routing through `Modules\Auth\Filters\Ci4MsAuthFilter`, which requires the role permission `fileeditor.delete` for `deleteFileOrFolder` and `fileeditor.update` for `renameFile`. Superadmins always pass; role-assigned users with only the Fileeditor permission can also reach the sink, exceeding the editor's apparent design intent (the allowlist on save/create signals that the editor is meant to handle only safe content-type files).PoCPrerequisites: an authenticated session with `fileeditor.delete` (or `superadmin`) for step 1, and `fileeditor.update` for step 2. The application is mounted under `backend/`, not `admin/`.Trace verifying the validation logic for `path=app/Config/Routes.php`:Regex `/^[a-zA-Z0-9_ \-\.\/]+$/` — matches.`isHiddenPath('app/Config/Routes.php')` — segments `['app','Config','Routes.php']`, none in `$hiddenItems` → returns `false`.`realpath(ROOTPATH . 'app/Config/Routes.php')` — resolves inside ROOTPATH, containment check passes.`unlink($fullPath)` (deleteFileOrFolder, line 229) or `rename($fullPath, $newPath)` (renameFile, line 146) executes — no extension allowlist applied.ImpactA backend user holding the Fileeditor `delete` or `update` permission can:Delete or neutralize the front controller (`public/index.php`), routing config (`app/Config/Routes.php`), database config (`app/Config/Database.php`), filter pipeline (`app/Config/Filters.php`), web-server rules (`public/.htaccess`), or any other framework file inside the project root.Cause persistent denial of service: the application becomes unreachable on the next request and there is no in-app "restore" — recovery requires filesystem access (redeploy, git checkout, or backup restore).Destroy data files inside the project tree (e.g. SQLite databases, cached config) outside the small `$hiddenItems` blocklist.The destructive surface exceeds Fileeditor's intended capability: the saveFile/createFile allowlist signals an explicit design intent to restrict modifications to safe content extensions, yet delete/rename can target arbitrary file types. Even where the actor is already a superadmin, the bug widens the destructive blast radius beyond what the editor UI exposes and beyond what `fileeditor.delete` plausibly authorizes for non-superadmin role holders.The path is gated by an admin-tier permission, so PR:H is honest; impact is limited to integrity/availability of files reachable by the web server user.Recommended FixApply the same `allowedFileTypes()` allowlist (or a stricter directory allowlist for editor-managed assets) to the source path in both destructive endpoints. After the existing `realpath` containment check:Stronger hardening — and aligned with the editor's apparent intent — is to confine all Fileeditor operations to a directory allowlist (e.g. `public/templates/`, `public/uploads/`) rather than the entire `ROOTPATH`, and to extend `$hiddenItems` (or replace it with a denylist of full path prefixes) so that `app/Config`, `public/index.php`, `public/.htaccess`, and similar framework artefacts cannot be reached even by symlink or alternate casing.
CVSS v3.1 score: 6.5