Skip to content

Confine asset combiner imports to allowed roots (GHSA-2223-f22x-24cq) - #1504

Open
LukeTowers wants to merge 2 commits into
developfrom
wip/ghsa-2223-js-importer-lfi
Open

Confine asset combiner imports to allowed roots (GHSA-2223-f22x-24cq)#1504
LukeTowers wants to merge 2 commits into
developfrom
wip/ghsa-2223-js-importer-lfi

Conversation

@LukeTowers

@LukeTowers LukeTowers commented Jul 26, 2026

Copy link
Copy Markdown
Member

Summary

Fixes GHSA-2223-f22x-24cq. A backend user with cms.manage_assets could upload a .js asset containing =include ../../../.env and have the combiner inline arbitrary server-readable files (APP_KEY, DB credentials, etc.) into output that SystemController@combine serves unauthenticated. This is the JavaScript sibling of the LESS import flaw (GHSA-58fp-mcx6-7qf9); that fix left the JS and CSS importers exploitable.

The core hardening lives in winter/storm (JavascriptImporter path confinement + .js extension gate, shared ImportGuard). This PR wires it into the combiner and closes the matching CSS gap.

Changes

  • CombineAssets defines the allowed import roots once (themes/plugins/modules) and shares them across:
    • the JS importer via setAllowedImportRoots(),
    • the CSS importer via Assetic's new opt-in import validator (PathResolver + ImportGuard),
    • the LESS compiler (unchanged roots, now from the shared list).
  • Permission warnings: cms.manage_assets and cms.manage_content are now flagged as trusted-user permissions in the backend UI. Both are classed as administrative in the security policy scoring guidance but, unlike the other CMS permissions, carried no warning comment.

Tests

New CombineAssetsTest cases for =include traversal blocking, the .js-only extension gate, and legitimate same-tree includes. 12/12 pass. Verified end-to-end: the .env PoC no longer leaks, while the real backend JS (winter.js, cross-tree ../../../system/... includes) and LESS bundles still combine with zero blocked imports.

Dependencies / merge order

Requires (release order):

  1. Add optional import validator to CssImportFilter assetic-php/assetic#51CssImportFilter::setImportValidator() (the CSS gate is inert until this ships and the constraint is bumped)
  2. wintercms/storm wip/ghsa-2223-js-importer-lfiJavascriptImporter confinement + ImportGuard
  3. this PR

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Security

    • Strengthened asset processing to block unsafe file imports and directory traversal attempts.
    • Continued to support valid imports within approved theme, plugin, and module locations.
  • Permissions

    • Added clearer descriptions for CMS content and asset management permissions, improving administrator understanding when configuring access.

A backend user with `cms.manage_assets` could upload a `.js` asset containing
`=include ../../../.env` and have the combiner inline arbitrary server-readable
files (APP_KEY, DB credentials, etc.) into output that
`SystemController@combine` serves unauthenticated. This is the JavaScript
sibling of the LESS import flaw (GHSA-58fp-mcx6-7qf9); that fix left the JS and
CSS importers exploitable.

- Configure the JS importer with the same allowed import roots already used for
  LESS (themes/plugins/modules), now that `JavascriptImporter` enforces path
  confinement (winter/storm).
- Confine Assetic's `CssImportFilter` via its new opt-in import validator,
  rejecting `@import` targets that escape the allowed roots.
- Define the allowed roots once and share them across the JS, CSS, and LESS
  filters.

Also flag `cms.manage_assets` and `cms.manage_content` as trusted-user
permissions in the backend UI. Both are classed as administrative in the
security policy's scoring guidance but, unlike the other CMS permissions,
carried no warning comment.

Adds regression coverage in CombineAssetsTest for `=include` traversal, the
`.js`-only extension gate, and legitimate same-tree includes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

CMS backend permissions now expose descriptive comments for content and asset management, with corresponding English translation strings. Combined asset processing now restricts JavaScript, CSS, and LESS imports to approved roots, validates CSS paths through PathResolver, and adds regression tests for traversal blocking, extension filtering, and valid same-tree includes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: restricting asset combiner imports to approved roots and references the related advisory.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wip/ghsa-2223-js-importer-lfi

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Follows the storm-side removal of ImportGuard: the combiner's CssImportFilter
validator now calls the generic PathResolver::withinAny() instead. No behaviour
change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modules/system/tests/classes/CombineAssetsTest.php`:
- Around line 193-195: Move temporary theme-directory cleanup from the three
finally blocks in modules/system/tests/classes/CombineAssetsTest.php at lines
193-195, 216-218, and 235-236 into the test class tearDown() method. At each
site, register $themeDir for teardown instead of calling
\File::deleteDirectory(), while retaining secret-file cleanup; ensure tearDown()
deletes all registered theme directories with \File::deleteDirectory().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f4f1a85-7a27-4c85-b5a9-5832fbea677c

📥 Commits

Reviewing files that changed from the base of the PR and between 743b726 and 021b549.

📒 Files selected for processing (4)
  • modules/cms/ServiceProvider.php
  • modules/cms/lang/en/lang.php
  • modules/system/classes/CombineAssets.php
  • modules/system/tests/classes/CombineAssetsTest.php

Comment on lines +193 to +195
} finally {
@unlink($secretPath);
\File::deleteDirectory($themeDir);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Move temporary theme-directory cleanup to tearDown().

  • modules/system/tests/classes/CombineAssetsTest.php#L193-L195: register $themeDir for teardown instead of deleting it in this finally.
  • modules/system/tests/classes/CombineAssetsTest.php#L216-L218: register $themeDir for teardown instead of deleting it in this finally.
  • modules/system/tests/classes/CombineAssetsTest.php#L235-L236: register $themeDir for teardown instead of deleting it in this finally.

As per coding guidelines, “Fixtures that flow through Assetic\Asset\FileAsset ... must live under base_path() ... and be cleaned up with \File::deleteDirectory() in tearDown().”

Proposed cleanup pattern
+protected $temporaryThemeDirs = [];
+
+protected function tearDown(): void
+{
+    foreach ($this->temporaryThemeDirs as $themeDir) {
+        \File::deleteDirectory($themeDir);
+    }
+
+    $this->temporaryThemeDirs = [];
+    parent::tearDown();
+}
+
 protected function makeTempThemeDir(): string
 {
     $themeDir = base_path('storage/framework/cache/security-tests/theme-' . bin2hex(random_bytes(4)));
     mkdir($themeDir . '/assets/less', 0777, true);
+    $this->temporaryThemeDirs[] = $themeDir;
+
     return $themeDir;
 }
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 193-193: Avoid unsafe call to unlink
Context: unlink($secretPath)
Note: [CWE-73] External Control of File Name or Path.

(avoid-unlink)

📍 Affects 1 file
  • modules/system/tests/classes/CombineAssetsTest.php#L193-L195 (this comment)
  • modules/system/tests/classes/CombineAssetsTest.php#L216-L218
  • modules/system/tests/classes/CombineAssetsTest.php#L235-L236
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/system/tests/classes/CombineAssetsTest.php` around lines 193 - 195,
Move temporary theme-directory cleanup from the three finally blocks in
modules/system/tests/classes/CombineAssetsTest.php at lines 193-195, 216-218,
and 235-236 into the test class tearDown() method. At each site, register
$themeDir for teardown instead of calling \File::deleteDirectory(), while
retaining secret-file cleanup; ensure tearDown() deletes all registered theme
directories with \File::deleteDirectory().

Source: Coding guidelines

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