Skip to content

Apply the style package allowlist to nested entries - #137

Merged
erseco merged 4 commits into
hotfix/test-isolationfrom
hotfix/style-package-nested-allowlist
Sep 26, 2026
Merged

erseco merged 4 commits into
hotfix/test-isolationfrom
hotfix/style-package-nested-allowlist

Conversation

@erseco

@erseco erseco commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #136.

Security fix: style package allowlist bypass

ExeLearning_Style_Package::verify_entries() skipped every entry that contained a / before checking its extension, whenever config.xml sat at the archive root. So a style ZIP could write x/shell.php or x/.htaccess into uploads/exelearning-styles/{slug}/x/. That directory has no deny-PHP guard, so the result was server-side code execution.

Uploading a style package requires manage_options. The fix matters most where that is not the same as full server access:

  • multisite sub-site admins;
  • sites with DISALLOW_FILE_MODS or DISALLOW_FILE_EDIT.

Change

  • Only directory entries (names ending in /) are skipped. Every file, at any depth, must have an allowed extension. The single-root check still runs first, so a directory outside the root folder (evil/ next to acme/) is rejected with zip_mixed_roots.
  • Rejection messages now esc_html() the entry name. Core prints settings errors unescaped, so a crafted entry name rendered as markup in wp-admin (a low-severity self-XSS).

Behaviour changes

  • Bug fix: single-root-folder packages that contain directory entries (acme/, acme/img/) are now accepted. Before, those entries failed the extension check, and most ZIP tools store them.
  • Root-config packages with extensionless or unlisted files in subfolders (for example fonts/LICENSE) are now rejected. The bundled eXeLearning themes only use allowed extensions.

Tests

  • New tests:
    • nested x/shell.php, x/.htaccess, x/.user.ini, deep/er/a.phtml and fonts/LICENSE are rejected;
    • directory entries are accepted, in both the root and the prefixed layout;
    • a directory outside the root folder is rejected;
    • the entry name is escaped in the rejection message.
  • StylesServiceTest's icon fixture no longer includes icons/no-extension, which only installed because of the bypass. readme.txt in the same fixture still covers skipping non-image files.
  • Full suite: OK (952 tests).

When config.xml sat at the archive root, verify_entries() skipped every entry
containing a slash before checking its extension, so a style ZIP could write
x/shell.php or x/.htaccess into uploads/exelearning-styles/{slug}/. Only
directory entries are skipped now; every file, at any depth, must have an
allowed extension. Directory entries in single-root-folder packages, which
were rejected as extensionless, are accepted too.

Also escape the entry name quoted in rejection messages: core prints settings
errors unescaped, so a crafted entry name ran as markup in wp-admin.
@github-actions

Copy link
Copy Markdown
Contributor

Test in WordPress Playground

Test the plugin with the code from this branch:

Preview in WordPress Playground

ℹ️ The eXeLearning editor is fetched from the shared release and unpacked into the plugin when the playground boots, so the first load may take a few extra seconds. ELP upload, shortcode, Gutenberg block and preview work normally.

@codecov-commenter

codecov-commenter commented Sep 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.87%. Comparing base (704e330) to head (f44aa80).

Additional details and impacted files
@@                     Coverage Diff                     @@
##             hotfix/test-isolation     #137      +/-   ##
===========================================================
- Coverage                    96.87%   96.87%   -0.01%     
  Complexity                     864      864              
===========================================================
  Files                           39       39              
  Lines                         4323     4322       -1     
===========================================================
- Hits                          4188     4187       -1     
  Misses                         135      135              
Flag Coverage Δ
javascript 95.70% <ø> (ø)
php 97.25% <100.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
includes/class-style-package.php 96.06% <100.00%> (-0.02%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@erseco
erseco added this pull request to stack #144 September 26, 2026 07:38
Skipping directory entries first let a directory outside the package's root
folder (evil/ next to acme/) pass verify_entries(), although it breaks the
single-root rule that zip_mixed_roots enforces. extract_safely() dropped it
anyway, but the check now runs in the right order.
@erseco
erseco merged commit 6793616 into main Sep 26, 2026
5 checks passed
@erseco
erseco deleted the hotfix/style-package-nested-allowlist branch September 26, 2026 08:47
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