Skip to content

Security: fix open advisories not covered by #455 and #456 - #457

Merged
jonasdekeukelaere merged 8 commits into
masterfrom
security-advisories-fixes
Oct 14, 2026
Merged

jonasdekeukelaere merged 8 commits into
masterfrom
security-advisories-fixes

Conversation

@jonasdekeukelaere

@jonasdekeukelaere jonasdekeukelaere commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Fixes for the open security advisories on forkcms/forkcms that are not covered by #455 (GenerateUrl reflection) and #456 (SVG sanitizing). One commit per issue, most severe first:

  1. MediaLibrary chunked upload: qquuid was used unchecked as a folder name and the combined file was written before the extension check, which allowed writing (or, through the cleanup step, deleting) arbitrary paths. Not reported in any advisory. Regression test added.
  2. Pages RemoveUploadedFile / UploadFile (GHSA-xv8g-294j-qxg8): type is now limited to UserTemplate, and both actions now run the CSRF check of AjaxAction (the uploader sends the header via customHeaders).
  3. Locale getTranslations (GHSA-mq46-r6w7-882w): languages and types are bound as placeholders instead of concatenated.
  4. Login redirect (GHSA-7rwh-76qw-gvrc, GHSA-cp6p-mv29-2m2r): backend and Profiles login only redirect to a local path (one leading /, no backslashes), validated after FILTER_SANITIZE_URL.
  5. Stored XSS in backend JS (GHSA-7gxh-j7f2-85p3, GHSA-q8pq-h344-f972): escape media item title/url, page extra labels, location titles, search synonyms and multipleTextbox values. utils.string.htmlEncode now also escapes quotes.
  6. GA/GTM ids (GHSA-rr72-p99g-v4h7): validate the id format. Low impact, as the same form already allows raw head scripts.

Testing

  • UploadHandlerTest fails without the fix and passes with it.
  • phpcs clean. Kernel/DB tests and PHPStan were not run locally (no parameters.yml), so CI needs to cover those.
  • Not tested in a browser: Pages template image upload/remove, media chooser, multipleTextbox (search synonyms, form builder) and the login redirect.

Summary by Sourcery

Harden file uploads, redirects, database queries, dynamic output, and tracking configuration against the remaining reported security vulnerabilities.

Bug Fixes:

  • Prevent arbitrary path writes and deletions through MediaLibrary chunked uploads by validating upload identifiers, checking extensions before file creation, and handling incomplete retries safely.
  • Protect Pages upload and removal actions with CSRF validation and restrict file operations to user templates.
  • Prevent SQL injection in locale translation searches by parameterizing language and type filters.
  • Prevent external login redirects by restricting backend and profile redirects to sanitized local paths.
  • Prevent stored cross-site scripting in backend-generated media, location, page, search, and form-builder content by consistently escaping output.
  • Validate Google Analytics and Google Tag Manager identifiers before embedding them in backend scripts.

Enhancements:

  • Extend backend JavaScript HTML encoding to escape both single and double quotes.
  • Add regression coverage for chunked upload path traversal, invalid extensions, successful uploads, and missing chunks.

Tests:

  • Add MediaLibrary upload handler regression tests covering unsafe UUIDs, disallowed extensions, valid files, and incomplete chunked uploads.

Jonas De Keukelaere added 6 commits October 13, 2026 11:44
…hunks

The qquuid was used unchecked as a folder name and the combined file was
written before its extension was validated.
RemoveUploadedFile used the type parameter unchecked in the file path and,
like UploadFile, skipped the csrf check of the parent AjaxAction.
Backslashes passed the old check, and browsers treat "/\" like "//".
Media item titles and urls, page extra labels, location titles, search
synonyms and multipleTextbox values were inserted without (complete)
escaping. htmlEncode now also escapes quotes so it is safe in attributes.
@sourcery-ai

sourcery-ai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

This security-focused PR addresses multiple independent advisories by validating filesystem and tracking inputs, parameterizing locale queries, enforcing CSRF and directory restrictions on Pages uploads, limiting login redirects to local paths, escaping backend-rendered values, and disabling SVG uploads in CKFinder; it also adds focused MediaLibrary upload regression tests.

Sequence diagram for secure chunked media upload

sequenceDiagram
    participant Client
    participant UploadHandler
    participant Filesystem

    Client->>UploadHandler: combineChunks(uploadDirectory, name)
    UploadHandler->>UploadHandler: getUuid()
    UploadHandler->>UploadHandler: checkFileExtension(name)
    alt invalid UUID or extension
        UploadHandler-->>Client: success false, preventRetry true
    else valid upload
        UploadHandler->>Filesystem: write combined file under validated UUID folder
        UploadHandler-->>Client: upload result
    end
Loading

Sequence diagram for CSRF-protected Pages file actions

sequenceDiagram
    participant Uploader
    participant UploadFile
    participant RemoveUploadedFile
    participant AjaxAction

    Uploader->>UploadFile: execute() with X-CSRF-Token
    UploadFile->>AjaxAction: parent.execute()
    AjaxAction-->>UploadFile: CSRF validated
    UploadFile-->>Uploader: upload response

    Uploader->>RemoveUploadedFile: execute() with file and type
    RemoveUploadedFile->>AjaxAction: parent.execute()
    AjaxAction-->>RemoveUploadedFile: CSRF validated
    RemoveUploadedFile->>RemoveUploadedFile: validate type as UserTemplate
    RemoveUploadedFile-->>Uploader: remove response
Loading

Sequence diagram for validated login redirect

sequenceDiagram
    participant User
    participant Login
    participant Filter
    participant Browser

    User->>Login: authenticate with redirect path
    Login->>Filter: filter_var(queryString, FILTER_SANITIZE_URL)
    Login->>Login: sanitizeQueryString(queryString, default)
    alt one leading slash and no backslashes
        Login->>Browser: redirect to local path
    else invalid or external path
        Login->>Browser: redirect to default local URL
    end
Loading

Flow diagram for safe backend output rendering

flowchart LR
    Input[Untrusted backend value] --> Encode[utils.string.htmlEncode]
    Encode --> Attribute[Escaped HTML attribute or text]
    Attribute --> Render[Backend UI]
    Encode --> Multiple[multipleTextbox values]
    Encode --> Media[Media titles and URLs]
    Encode --> Location[Location titles]
Loading

File-Level Changes

Change Details Files
Hardened chunked media uploads against path traversal and unsafe file creation.
  • Validate qquuid against the expected UUID format before using it as a directory name.
  • Reject disallowed extensions before combining chunks.
  • Add regression coverage for traversal, extension rejection, and valid uploads.
src/Backend/Modules/MediaLibrary/Component/UploadHandler.php
src/Backend/Modules/MediaLibrary/Tests/Component/UploadHandlerTest.php
Restricted Pages upload endpoints and protected them with CSRF validation.
  • Allow only the UserTemplate upload directory.
  • Invoke AjaxAction CSRF handling for upload and removal actions.
  • Send the CSRF token with template image uploads.
src/Backend/Modules/Pages/Ajax/RemoveUploadedFile.php
src/Backend/Modules/Pages/Ajax/UploadFile.php
src/Backend/Modules/Pages/Js/Pages.js
Removed SQL injection risk from locale translation filtering.
  • Generate placeholder lists for language and type filters.
  • Bind language, name, value, and type parameters in matching order.
src/Backend/Modules/Locale/Engine/Model.php
Constrained authentication redirects to sanitized local paths.
  • Sanitize redirect values before validation.
  • Require exactly one leading slash and reject backslashes to prevent scheme-relative or browser-normalized external redirects.
src/Backend/Modules/Authentication/Actions/Index.php
src/Frontend/Modules/Profiles/Actions/Login.php
Escaped user-controlled values in backend-generated HTML and JavaScript.
  • Expand htmlEncode to escape both quote types.
  • Escape media titles and URLs, page extra labels, location titles, search synonyms, and multipleTextbox values at insertion points.
src/Backend/Core/Js/utils.js
src/Backend/Core/Js/jquery/jquery.backend.js
src/Backend/Modules/Location/Js/Location.js
src/Backend/Modules/MediaLibrary/Js/MediaLibraryHelper.js
src/Backend/Modules/Pages/Js/Pages.js
src/Backend/Modules/Search/Actions/Synonyms.php
Validated tracking identifiers before embedding them in scripts.
  • Apply format validation to Google Analytics tracking IDs.
  • Apply GTM-specific format validation to container IDs.
src/Backend/Modules/Settings/Actions/Index.php
Disabled SVG uploads through CKFinder configurations.
  • Remove svg from general file and image allowed-extension lists without altering existing uploaded files.
src/Backend/Core/Js/ckfinder/config.php

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/Backend/Modules/Pages/Ajax/UploadFile.php" line_range="38" />
<code_context>
+
     public function execute(): void
     {
+        parent::execute();
+
         $request = $this->getRequest();
</code_context>
<issue_to_address>
**SVG upload remains enabled**

When a user with access to the Pages image uploader uploads an SVG and a same-origin visitor opens the stored file, `UploadFile::writeFile()` accepts `svg` in `ALLOWED_EXTENSIONS` and saves the uploaded bytes without sanitizing them, so an authenticated uploader can store an executable SVG under the publicly served Pages files path and expose same-origin visitors to script execution.

Reject SVG uploads in `UploadFile` or sanitize them before saving.
</issue_to_address>

### Comment 2
<location path="src/Backend/Modules/MediaLibrary/Js/MediaLibraryHelper.js" line_range="1547" />
<code_context>

     if (mediaItem.type === 'image') {
-      html += '<img src="' + mediaItem.preview_source + '" alt="' + mediaItem.title + '" title="' + mediaItem.title + '"/>'
+      html += '<img src="' + mediaItem.preview_source + '" alt="' + utils.string.htmlEncode(mediaItem.title) + '" title="' + utils.string.htmlEncode(mediaItem.title) + '"/>'
     } else {
       html += '<div class="icon"></div>'
</code_context>
<issue_to_address>
**Media preview URL enables XSS**

When an attacker able to upload media supplies a valid image with a filename containing attribute-breaking quotes, `preview_source` remains interpolated raw into each image’s `src` attribute. An attacker who uploads a valid image with a filename containing quotes and an event-handler attribute stores those characters in the media URL, so opening the media chooser parses attacker-controlled markup and runs the handler.

HTML-attribute-encode `preview_source` at each `src` interpolation, and ensure stored media URLs are safely encoded for their URL context.

Also at `src/Backend/Modules/MediaLibrary/Js/MediaLibraryHelper.js:1583`, `src/Backend/Modules/MediaLibrary/Js/MediaLibraryHelper.js:1611`.
</issue_to_address>

### Comment 3
<location path="src/Backend/Modules/MediaLibrary/Component/UploadHandler.php" line_range="63" />
<code_context>
         }
+
+        try {
+            $uuid = $this->getUuid();
+            $this->checkFileExtension($name);
+        } catch (Exception $e) {
</code_context>
<issue_to_address>
**Retried completion creates empty upload**

When a duplicate or retried `done` request runs after another completion request has consumed the chunks, `combineChunks` opens the destination with `wb` before checking that the chunk files still exist, then reports success even if reopening chunks fails. If a completion request is retried after another request consumed the chunks, the retry creates an empty file and the upload endpoint can dispatch it as a media item.

Make chunk combination atomic and idempotent: claim the UUID once, verify all chunks and writes, and return the prior result or an error for subsequent completion requests.
</issue_to_address>

Sourcery assessment

Approval pending. 3 findings to address first.

Blocking findings: src/Backend/Modules/Pages/Ajax/UploadFile.php:38, src/Backend/Modules/MediaLibrary/Js/MediaLibraryHelper.js:1547, src/Backend/Modules/MediaLibrary/Component/UploadHandler.php:63


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/Backend/Modules/Pages/Ajax/UploadFile.php
Comment thread src/Backend/Modules/MediaLibrary/Js/MediaLibraryHelper.js Outdated
Comment thread src/Backend/Modules/MediaLibrary/Component/UploadHandler.php
Comment thread src/Frontend/Modules/Profiles/Actions/Login.php
'name' => 'Files', // Single quotes not allowed.
'directory' => 'files',
'maxSize' => 0,
'allowedExtensions' => '7z,aiff,asf,avi,bmp,csv,doc,docx,eps,fla,flv,gif,gz,gzip,jpeg,jpg,mid,mov,mp3,mp4,mpc,mpeg,mpg,ods,odt,pdf,png,ppt,pptx,pxd,qt,ram,rar,rm,rmi,rmvb,rtf,sdc,sitd,svg,swf,sxc,sxw,tar,tgz,tif,tiff,txt,vsd,wav,webp,wma,wmv,xls,xlsx,zip',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dat gaat opgelost zijn met #456
Dus zou dat hier niet weghalen/zo oplossen.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Klopt, ik heb de CKFinder-wijziging uit deze PR en uit de upstream-PR gehaald. Let wel: #456 raakt CKFinder momenteel niet aan (enkel MediaLibrary en Pages UploadFile), dus CKFinder aanvaardt nog steeds ongesanitizede SVG's van elke ingelogde backend-gebruiker (GHSA-fjq6-2rhm-4g37). @absumo, kan je CKFinder mee opnemen in #456?

@jonasdekeukelaere
jonasdekeukelaere force-pushed the security-advisories-fixes branch from 776c1a7 to 3032295 Compare October 13, 2026 12:37
@jonasdekeukelaere
jonasdekeukelaere merged commit a0d84cb into master Oct 14, 2026
8 of 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