Skip to content

fix(files): enforce file groups and permissions in the file manager, file browser and document save - #2480

Merged
Seiger merged 4 commits into
evolution-cms:3.5.xfrom
elcreator:fix-file-and-image-browser-acl
Sep 27, 2026
Merged

Seiger merged 4 commits into
evolution-cms:3.5.xfrom
elcreator:fix-file-and-image-browser-acl

Conversation

@elcreator

Copy link
Copy Markdown

Summary

Fixes authorization gaps in the classic file manager (a=31), the embedded file browser (manager/media/browser/mcpuk) and the document save path. The two tools stay independent: the file manager is rooted at filemanager_path; the browser at rb_base_dir, optionally narrowed per manager by image_base_upload_dir.

File manager and browser: file groups (use_udperms)

  • Sibling prefix. Root checks were strpos($path, $root) === 0, so a root at assets/alice also admitted assets/alice-private. They now use FileManagerAccess::isWithin().
  • Raw path vs. resolved target. The classic manager checked the raw request (own/../private) against the listing's restriction map, then acted on the resolved target. Actions now resolve first, then look the target's groups up fresh: delete folder, duplicate, rename file and folder, view/edit, the groups page and saving it, and unzip.
  • Recursive operations.
    • Deleting a folder is refused when it holds entries the manager may not reach.
    • The directory zip filters against the whole subtree; before, it only filtered the current folder's own entries.
    • Unzip checks access to the archive itself.
  • Embedded browser reads. Groups were only applied when listing. Every action that names an entry now checks it: thumbnail, download, rename, delete, clipboard copy/move/delete, folder delete, and the folder, selected-files and clipboard zips.
  • Keys. file_groups rows are now keyed by the site-wide filemanager_path. Before, a manager's own filemanager_path gave every path a different key, so groups set by an admin were silently ignored for that manager in both tools.
  • Rows follow renames, moves and deletes, whoever performs them.
    • The file manager moved rows only for group managers; the browser never did. Renaming a parent folder made everything below it public.
    • The LIKE match no longer rewrites siblings through _ or %.
  • ls() was never given the manager's groups, so it hid entries the manager may see.

Embedded browser: types and root

  • Every type needs a permission. browse.php checked only type=images and type=files, so media, image, file, a missing type and an unknown one needed nothing (the last two open the files folder). Image types now need assets_images and all others assets_files; file_manager still opens everything.
  • An unusable image_base_upload_dir is refused. A value outside the site, or one using .., used to fall back to the whole rb_base_dir, handing a confined editor everyone's folders. The refusal happens before the uploader touches the disk.
  • The root test was a prefix match, so assets2 was treated as a folder below assets (URL assets/2).
  • The refusal message was an empty 200 page, because there is no global $_lang in the browser. It now uses the translator and answers 403.

Document save

  • The editor page checks access to the document, but a posted save only checked save_document, and the parent only when it changed. The save service now checks the document itself when use_udperms is on.

Classmap

  • core/vendor is committed and classmap-authoritative, and EvolutionCMS\Tracy\ConnectionTiming was missing from it. A new test reads the committed classmap from git and requires every class under core/src to be in it. The new FileBrowserAccess is added.

Settings reference (unchanged behaviour, for review)

Setting Tool Default Per manager
filemanager_path File manager (a=31, needs file_manager) [(base_path)] yes
rb_base_dir / rb_base_url Embedded browser (TinyMCE, image TVs) [(base_path)]assets/ / assets/ yes
image_base_upload_dir Embedded browser, per-manager sub-root empty yes; relative to rb_base_dir or absolute inside the site

To give each editor their own part of the images, set image_base_upload_dir per manager (e.g. alice gives assets/alice/images).

Test plan

  • Unit tests: FileManagerAccessTest (containment, .. resolution, subtree restrictions, ACL keys, row move/forget including _ siblings), FileBrowserAccessTest (type permissions, root resolution incl. fail-closed cases), DocumentSaveServiceTest (edit denied on an unreachable document), FileManagerAclSourceTest (every action's check), CommittedClassmapTest. The new tests fail on 3.5.x.
  • Full Pest suite green; PHPStan: no errors.
  • Manually on a SQLite install with use_udperms, an HR and an Editors group, and non-admin managers:
    • saving the HR-only document is refused, a public one saves
    • team/../hr/salaries.txt and ../alice-private are denied
    • deleting a folder containing HR files is denied; a clean one deletes
    • both zips leave out HR entries
    • HR thumbnails and downloads are refused
    • a manager with a personal filemanager_path no longer sees HR images
    • image_base_upload_dir=/etc answers 403 and creates nothing
    • an images-only editor gets 403 for files, file, media, missing and unknown types
    • renaming a folder in the browser moves its HR rows

🤖 Generated with Claude Code

elcreator and others added 4 commits September 26, 2026 23:01
… and browser

- Root checks used a bare prefix match, so a root at assets/alice also
  admitted assets/alice-private. FileManagerAccess::isWithin() replaces them.
- Classic file manager actions checked the raw request (own/../private)
  against the listing's restriction map, then acted on the resolved target.
  They now resolve first and look the target's groups up fresh.
- Deleting a folder is refused when it holds entries the manager may not
  reach; unzip checks the archive itself; the directory zip filters against
  the whole subtree, not only the current folder's entries.
- The embedded browser checked groups only when listing: thumbnail,
  download, rename, delete, clipboard copy/move/delete, folder delete and
  all three zip downloads now check the named entry.
- file_groups rows are keyed by the site-wide filemanager_path. A manager's
  own filemanager_path gave every path another key, which nobody had
  restricted, and switched groups off for them in both tools.
- Rows follow renames, moves and deletes whoever performs them; before,
  only group managers moved them in the file manager and the browser never
  did, so renaming a parent folder made everything below it public. The
  LIKE match no longer catches siblings through _ or %.
- ls() was never given the manager's groups, hiding entries they may see.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unusable root

- browse.php checked assets_images/assets_files only for type=images and
  type=files. media, image, file, a missing type and an unknown one (both
  open the files folder) needed nothing. Every type now maps to a
  permission; file_manager still opens all of them.
- A manager whose image_base_upload_dir could not be resolved (outside the
  site, or climbing out with ..) was silently given the whole rb_base_dir,
  with every other manager's folders in it. The browser now refuses, before
  the uploader creates folders or prunes archives under an empty root.
- The root test was a bare prefix match; assets2 was taken for a folder
  below assets and got the URL assets/2.
- The refusal printed an empty page: there is no global $_lang in the
  browser. It now uses the translator and answers 403.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The editor page refuses a document the manager's groups do not reach, but a
posted save only checked save_document, and the parent only when it
changed. A manager who knew a restricted document's id and parent could
overwrite it. The save service now checks the document itself when
use_udperms is on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The committed autoloader is classmap-authoritative, and
EvolutionCMS\Tracy\ConnectionTiming was missing from it, so
TracyServiceProvider failed with "class not found" on a site while tests,
with a regenerated vendor/, passed. A test now reads the committed
classmap from git and requires every class under core/src in it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Seiger
Seiger merged commit 292915f into evolution-cms:3.5.x Sep 27, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants