Skip to content

Fix manager privilege escalation - #2493

Merged
Seiger merged 7 commits into
evolution-cms:3.5.xfrom
elcreator:fix-manager-privilege-escalation
Oct 8, 2026
Merged

Seiger merged 7 commits into
evolution-cms:3.5.xfrom
elcreator:fix-manager-privilege-escalation

Conversation

@elcreator

Copy link
Copy Markdown

No description provided.

elcreator and others added 7 commits October 8, 2026 16:00
Validate every dot-separated part of an uploaded, renamed, copied or
moved file name against the denied extensions list, not just the
final extension - a name like shell.php.jpg passed before. Also
require that a name claiming a raster image format (jpg, png, gif,
webp, bmp, ico, avif, tif, psd) actually decodes as that format.

Extend the file manager's ZIP extractor to skip entries whose name
would be treated as server-executable (a PHP-like extension anywhere
in the name, or .htaccess/.user.ini/etc.), closing the same bypass
there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@eval (PHP eval()) and @select (raw SQL) were reachable from any
template variable value or definition field, and from resource
content via @DOCUMENT/@inherit, with no gate beyond save_template or
save_document. A manager with only those permissions could store a
binding that runs when the TV renders.

Add TvBindingGuard, which recognizes @EVAL/@select by the same
prefix match the parser uses (no word boundary, since the parser has
none either - "@EVALreturn 1;" still reaches eval()), looks through
any @inherit chain, and also checks the page text fields a binding's
[*field*] placeholder can pull in. A manager without save_snippet may
not add or change a binding; an existing one some other editor
stored is left alone.

Apply it in DocumentSaveService (resource save) and
save_tmplvars.processor.php (TV definition save).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Page 118 maps to a plain view (ManagerTheme::handle() renders it
without a controller permission check), and that view includes
mutate_settings.ajax.php, which let any authenticated manager change
a system setting or update/delete a plugin - no settings, save_plugin
or delete_plugin check.

Require settings for setsetting, except keys starting with _hide_
(client-side notice-dismiss flags, harmless and meant to be open to
every manager), and require save_plugin or delete_plugin for
updateplugin, limited to the disabled column or the _delete_ action.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TemplateVariableValues::forTemplate() joined document_groups on the
saved resource and used that join to decide whether a non-admin
manager could see a TV restricted by site_tmplvar_access. That
checks the document's groups, not the TV's: a manager whose groups
overlap the resource's groups could get a TV restricted to some
other, unrelated group into the editable set.

Join site_tmplvar_access.documentgroup against the manager's own
groups instead, dropping the document_groups join entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
resolveAtBindFilePath() blocked traversal out of the install and
reads from manager/, but not from core/ - a @file or @include
binding, or the file_get_contents/readfile modifier, could still
reach core/.env, core/config, core/custom and other core files that
happen to sit below the web root. Refuse core/ the same way manager/
already was, falling back to doing nothing when it is not actually
below the base path (a relocated core).

Add Core::atBindFileIsReadable(): even inside the allowed area,
@file and the file_get_contents/readfile modifier must not hand out
PHP-like source (snippets, plugins, modules - anywhere, not just a
fixed extension list), hidden files/folders, assets/cache,
assets/backup, or files like .env/.ini/.sql/.log/.bak/.conf/.pem/.key.
Apply it to the TV custom widget's @file branch too, which had no
extension check of its own. @include is unaffected beyond the core/
block - it exists to run PHP, so it still may.

The modifier's error messages now say why a read was refused (denied
extension, hidden/generated file, or outside the allowed area)
instead of a single generic message, without naming the path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The preview iframe loaded same-origin, with no sandbox, so script in
previewed content (which a lower-privileged editor can supply, since
HTML in resources is intentionally allowed) could reach window.top,
read the manager's CSRF meta tag, and ride a previewing manager's
session into a same-origin request. The session cookie is already
HttpOnly, so this is request forgery via a stolen token, not cookie
theft.

Add sandbox="allow-scripts allow-forms allow-popups", deliberately
without allow-same-origin: previewed script and form submission
still work, but the frame gets an opaque origin and can no longer
reach outside itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
main.css stretches .warning to ~100% width whenever it sits as a
direct child of td:first-child in these forms (#documentPane,
#webUserPane) - meant for a label whose whole text is the warning.
The web user form's required-email row only wrapped the "*" in
.warning, so that rule stretched the lone asterisk across almost the
entire first column and pushed "E-mail address:" out to the right,
behind the input field.

Wrap the whole label in an outer span so .warning is no longer a
direct child of the td and the rule stops matching, same as every
other label row in this form.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Seiger
Seiger merged commit ab0ecf4 into evolution-cms:3.5.x Oct 8, 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