Skip to content

fix(manager): close CSRF gaps, escape log filters and neutralise test payloads - #2478

Open
elcreator wants to merge 5 commits into
evolution-cms:3.5.xfrom
elcreator:fix-xss-and-payloads-in-tests
Open

elcreator wants to merge 5 commits into
evolution-cms:3.5.xfrom
elcreator:fix-xss-and-payloads-in-tests

Conversation

@elcreator

@elcreator elcreator commented Sep 26, 2026 •

Copy link
Copy Markdown

Summary

CSRF: roles, permissions, permission groups and categories could be deleted cross-site

  • UserRole (a=35/36/38), Permission (a=135), PermissionsGroups (a=136) and the category manager (a=120/121) delete straight from GET parameters, and none of these actions required a token.
  • These actions also render their edit pages, so they can't go into MUTATING_GET_ACTIONS without demanding a token for plain navigation. A new VerifyCsrfToken::MUTATING_GET_PARAMETERS list verifies them only when the delete trigger (?action=, module_categories_manager[...]) is in the query string.
  • The five delete links (role, permission and group editors, the permission list, and the category editor) now send _token.

CSRF: OPcache reset (a=53)

  • SystemInfo calls opcache_reset() whenever opcache_reset is in the request. Now a=53 needs a token when that parameter is present, and the reset link sends it. Opening the System Info page still needs no token.

CSRF: logout (a=8) and module dependencies (a=113)

  • Module dependencies are added and removed from $_REQUEST['op'], so a plain GET changed them. Now a=113 needs a token when op is present.
  • Logout destroyed the session on GET. Now a=8 is a guarded action, and every logout link sends the token: the frame, the welcome page, [+logouturl+] and the keepalive redirect.

XSS: category manager, web users, page titles

  • Category manager: ?id= was reflected into every form action. It is now cast to an integer.
  • Web-user list (a=99): the reflected ?search= box and the stored username, full name, email and role names are escaped. Web users can register themselves, so an anonymous visitor could reach an admin's screen through these fields.
  • Web-user editor header: the username is escaped. Before, it was html_entity_decoded and then printed raw.
  • Menu-index sort, delete-template and delete-TV screens: page titles are escaped. Summaries go through sanitize_inline_html(), so their inline formatting still renders.
  • Escaping never double-encodes, so values that older installs stored entity-encoded still show the same text.

XSS: manager log (a=13)

  • ?message= was echoed raw into value="". The global sanitizer only rewrites <script, so " autofocus onfocus=... got through.
  • Every filter was also concatenated into the pagination links. Encoding them in the log page couldn't fix that, because Paginate urldecode()s its extra arguments, so Paginate now escapes them itself.
  • The stored username, itemname and itemid values in the filter dropdowns were printed raw (stored XSS through document and element names). They are now escaped.

Tests: payloads that trigger antivirus scanners

  • The octal-obfuscated system("id"), phpinfo, backtick and file_put_contents payloads in ArithmeticExpressionTest are now built at runtime from harmless function names. They still contain no letters, so they still get past the callers' filter.
  • The cookie-exfiltration script, alert(document.cookie) and the rm -rf / string are replaced with inert markers.
  • Fixes BladeStoredXssTest: unescaped quotes had turned most of the stored-report payload into a PHP comment, so that test never contained the payload it claimed to test.

Test plan

  • New middleware tests: deletes and the OPcache reset are rejected without a token, deletes with a token pass, and edit pages need no token
  • New coverage tests pin the delete branches to the middleware list and require _token on every delete link
  • Display tests: raw and legacy entity-encoded values render as the same visible text, the web-user list never double-encodes, and summaries keep their formatting
  • New XSS tests on logging.static.php, plus behavioural Paginate tests (hostile input stays inside href; filter values survive the round trip)
  • Full Pest suite: 970 passed. The one failure, ApacheConfigHardeningTest, comes from running outside a git checkout
  • PHPStan clean on the changed classes
  • Checked on a Docker install as a logged-in admin: tokenless deletes return 403 and leave the rows in place, tokened deletes work, edit pages return 200, and log payloads come out escaped

🤖 Generated with Claude Code

elcreator and others added 3 commits September 26, 2026 15:29
…log filters

Roles, permissions, permission groups and categories were deleted from GET
parameters on actions that also render their edit pages, so none of them
required a CSRF token. The middleware now verifies those actions when the
triggering query parameter is present, and the delete links send the token.

The manager log echoed the message filter raw into value="", and every filter
reached the pagination links unescaped because Paginate urldecodes its extra
arguments. Paginate now escapes them itself, and the stored item and user
names in the filter dropdowns are escaped too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Octal-obfuscated function calls, a cookie-exfiltration script and an
rm -rf string match antivirus signatures. The arithmetic payloads are now
built at runtime from harmless function names, and the others use inert
markers. Also repairs the stored error report payload, whose unescaped
quotes had turned most of it into a PHP comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SystemInfo (a=53) resets OPcache whenever opcache_reset is in the request,
and neither the action nor its reset link carried a token. The middleware
now verifies a=53 when the parameter is present, and the link sends the token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@elcreator elcreator changed the title fix(manager): guard role, permission and category deletes and escape log filters fix(manager): close CSRF gaps, escape log filters and neutralise test payloads Sep 26, 2026
elcreator and others added 2 commits September 26, 2026 16:09
…anges

Module dependencies (a=113) are added and removed from $_REQUEST['op'], so a
plain GET changed them. The middleware now verifies a=113 when op is present.

Logout (a=8) destroyed the manager session on GET without a token. It is now
a guarded action, and the logout links in the frame, the welcome page, the
lockout placeholder and the keepalive redirect carry the token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- category manager: cast ?id= before it is written into every form action
- web user list: escape the reflected search box and the stored username,
  full name, email and role names
- web user editor: escape the username shown in the header
- menu index sort and the template/TV delete screens: escape page titles and
  sanitise summaries while keeping their inline formatting

Escaping never double-encodes, so values stored entity-encoded by older
installations still render as the same text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant