aregowe/magento2-module-polyshell-protection

Comprehensive defense-in-depth module that closes the PolyShell unrestricted file upload vulnerability (APSB25-94) in Adobe Commerce and Magento Open Source.

Maintainers

Package info

github.com/aregowe/magento2-module-polyshell-protection

Type:magento2-module

pkg:composer/aregowe/magento2-module-polyshell-protection

Statistics

Installs: 579

Dependents: 0

Suggesters: 0

Stars: 2

Open Issues: 0

1.1.0 2026-04-14 22:23 UTC

This package is auto-updated.

Last update: 2026-04-14 22:24:25 UTC


README

Purpose

Comprehensive defense-in-depth module that closes the PolyShell unrestricted file upload vulnerability (APSB25-94) in Adobe Commerce. PolyShell affects all Magento Open Source and Adobe Commerce versions up to 2.4.9-alpha2. No official isolated patch exists for production versions.

This module was originally forked from markshust/magento2-module-polyshell-patch by Mark Shust. With Mark's permission, his module's logic has been fully integrated into this one, and he has deprecated his package in favor of this module.

Reference: Sansec — PolyShell: unrestricted file upload in Magento and Adobe Commerce

If this module helped protect your store, consider buying me a coffee ☕ — it helps me keep maintaining and improving it.

Installation

Via Composer (recommended)

composer require aregowe/magento2-module-polyshell-protection
bin/magento module:enable Aregowe_PolyShellProtection
bin/magento setup:upgrade
bin/magento cache:flush

Manually

Copy the module into your project:

mkdir -p app/code/Aregowe/PolyShellProtection
cp -r * app/code/Aregowe/PolyShellProtection/
bin/magento module:enable Aregowe_PolyShellProtection
bin/magento setup:upgrade
bin/magento cache:flush

Uninstallation

bin/magento module:disable Aregowe_PolyShellProtection
bin/magento setup:upgrade
composer remove aregowe/magento2-module-polyshell-protection
bin/magento cache:flush

Migrating from MarkShust_PolyshellPatch

This module integrates and supersedes markshust/magento2-module-polyshell-patch. You do not need both modules — this one includes all of Mark Shust's original protection and extends it significantly.

Mark Shust's module provided a focused two-plugin fix that enforced a 4-extension image allowlist (jpg, jpeg, gif, png) on ImageContentValidator and ImageProcessor. That logic is now fully integrated into this module's HardenImageContentValidatorPlugin and HardenImageProcessorPlugin, which add:

  • Polyglot file scanning (detects valid images with embedded PHP)
  • No-extension and double-extension attack detection
  • Multi-pass URL decoding and obfuscation normalization
  • Known attack filename/pattern matching
  • Request path blocking at the FrontController and pub/get.php level
  • Controller-level upload blocking for customer attribute and file upload endpoints
  • A kill switch blocking all custom option file uploads via the Webapi File Processor

The composer.json includes a "replace" directive for markshust/magento2-module-polyshell-patch, so Composer will automatically handle the transition.

If you currently have MarkShust_PolyshellPatch installed

bin/magento module:disable MarkShust_PolyshellPatch
bin/magento setup:upgrade
composer require aregowe/magento2-module-polyshell-protection
bin/magento module:enable Aregowe_PolyShellProtection
bin/magento setup:upgrade
bin/magento cache:flush

Composer's replace directive will remove MarkShust's package automatically when this module is installed.

Credits

This module was forked from markshust/magento2-module-polyshell-patch, created by Mark Shust and sponsored by M.academy. Mark gave his permission to integrate his module's logic into this one and has deprecated his package in favor of this project. Thank you, Mark!

Vulnerability Summary

Magento's REST API accepts file uploads as part of cart item custom options. When a product option has type file, Magento processes an embedded file_info object containing base64-encoded file data, a MIME type, and a filename. The file is written to pub/media/custom_options/quote/ on the server.

Three critical checks are missing from core Magento:

  1. No option ID validation — the submitted option ID is never verified against the product's actual options.
  2. No option type gating — file upload logic triggers regardless of whether the product has a file-type option.
  3. No file extension restriction — extensions like .php, .phtml, and .phar are not blocked. The only validation is getimagesizefromstring, which is trivially bypassed using polyglot files (valid image headers containing embedded PHP).

The most dangerous endpoints are the anonymous guest cart routes:

Method Endpoint Auth Required
POST /V1/guest-carts/:cartId/items None
PUT /V1/guest-carts/:cartId/items/:itemId None

Live Attack Patterns

Attackers upload polyglot files — valid GIF or PNG images containing executable PHP. Two payload types are in active use:

  • Cookie-authenticated webshell — GIF89a polyglot dropped as index.php, verifies cookie against hardcoded MD5 hash, executes arbitrary code via eval(base64_decode()).
  • Password-protected RCE shell — uses hash_equals() with double-MD5 hash, passes commands to system().

Common attack filenames: index.php, 780index.php (option_id prefix), json-shell.php, bypass.phtml, rce.php, shell.php, accesson.php, test.php, ato_poc.html.

Post-exploitation deploys accesson.php backdoors across writable directories (app/assets/images/, var/assets/images/, vendor/assets/images/, etc.) and injects JavaScript malware loaders into CMS content.

How This Module Protects

This module implements eight layered Magento plugins and three security models that block the attack at every interception point. If one layer is bypassed, subsequent layers catch it.

Defense Layers (in execution order)

Layer 1 — Request Path Blocking

Plugin Target Class Strategy
BlockSuspiciousMediaPathPlugin FrontController Blocks HTTP requests to /media/customer_address/, /media/custom_options/, etc. via aroundDispatch. Returns 404.
BlockSuspiciousMediaAppPathPlugin Media (get.php) Blocks media serving via the pub/get.php entrypoint for the same paths. Uses reflection to read Media::$relativeFileName. Returns 404.

Layer 2 — Controller-Level Upload Blocking

Plugin Target Class Strategy
BlockCustomerAttributeFileUploadControllerPlugin AbstractUploadFile Blocks ALL customer attribute file upload controllers at the entry point. Returns JSON error.
BlockCustomerFileUploadPlugin FileProcessor Blocks saveTemporaryFile and moveTemporaryFile for customer_address, customer_addresses, and custom_options entity types. Fails closed if reflection cannot read entity type.

Layer 3 — Custom Option Upload Blocking

Plugin Target Class Strategy
ValidateUploadedFileNamePlugin File\Processor (Webapi) Kill switch — unconditionally blocks ALL custom option file uploads via the Webapi File Processor.
ValidateUploadedFileContentPlugin File\Processor (Webapi) Validates filename safety (extension, pattern, obfuscation) and scans file content for polyglot/embedded PHP.
ValidateCustomOptionUploadPlugin CustomOptionProcessor Detects file payloads in cart item custom options and blocks the entire request.

Layer 4 — Framework-Level Image Hardening

Plugin Target Class Strategy
HardenImageContentValidatorPlugin ImageContentValidator After core validation, enforces strict image-only extension allowlist, blocks files with no extension, detects double-extension attacks (.php.jpg), scans base64 content for polyglot payloads. Integrates MarkShust_PolyshellPatch's extension check.
HardenImageProcessorPlugin ImageProcessor Before file write, locks the Uploader's allowed extensions via reflection, validates filename, blocks non-image extensions, scans for polyglot content.

Security Models

Model Responsibility
FileUploadGuard Orchestrates filename validation: extension allowlist, blocked-extension pattern matching, normalization (unicode decoding, multi-pass URL decoding, control character removal), and delegates to AttackPatternDetector and PolyglotFileDetector.
AttackPatternDetector Maintains a list of known attack filenames and regex patterns observed in active PolyShell campaigns. Blocks exact filename matches and suspicious patterns (option_id + index.php, double extensions, shell/backdoor naming, obfuscation hints).
PolyglotFileDetector Detects polyglot files by checking if content starts with an image signature (PNG, GIF, JPEG, RIFF, ICO, CUR, BMP) and then scanning for embedded PHP code patterns (<?php, eval(, system(, exec(, etc.) and known attack beacon signatures (409723*20, campaign-specific MD5 hashes).
SecurityPathGuard Evaluates request paths and media-relative paths against blocked directory prefixes (/media/customer_address, /media/custom_options, etc.).
SecurityLogSanitizer Sanitizes log context values — strips control characters, collapses whitespace, enforces maximum length — to prevent log injection attacks.

Additional Defenses

  • Nginx deny rules — recommended in tandem with this module to block direct access to pub/media/custom_options/ at the web server level.

Module Structure

app/code/Aregowe/PolyShellProtection/
├── etc/
│   ├── module.xml                     # Module declaration
│   └── di.xml                         # Plugin wiring and DI configuration
├── Logger/
│   ├── Logger.php                     # Dedicated Monolog logger channel
│   └── Handler/
│       └── SecurityHandler.php        # Writes to var/log/polyshell_security.log
├── Model/
│   ├── AttackPatternDetector.php      # Known attack filenames and regex patterns
│   ├── FileUploadGuard.php            # Orchestrator: extension, pattern, content checks
│   ├── PolyglotFileDetector.php       # Image signature + embedded PHP detection
│   ├── SecurityLogSanitizer.php       # Log context sanitization
│   └── SecurityPathGuard.php          # Request path blocking rules
├── Plugin/
│   ├── BlockCustomerAttributeFileUploadControllerPlugin.php
│   ├── BlockCustomerFileUploadPlugin.php
│   ├── BlockSuspiciousMediaAppPathPlugin.php
│   ├── BlockSuspiciousMediaPathPlugin.php
│   ├── HardenImageContentValidatorPlugin.php
│   ├── HardenImageProcessorPlugin.php
│   ├── ValidateCustomOptionUploadPlugin.php
│   ├── ValidateUploadedFileContentPlugin.php
│   └── ValidateUploadedFileNamePlugin.php
├── Test/Unit/
│   ├── Model/
│   │   ├── AttackPatternDetectorTest.php
│   │   ├── FileUploadGuardTest.php
│   │   ├── PolyglotFileDetectorTest.php
│   │   ├── SecurityLogSanitizerTest.php
│   │   └── SecurityPathGuardTest.php
│   └── Plugin/
│       ├── BlockCustomerAttributeFileUploadControllerPluginTest.php
│       ├── BlockCustomerFileUploadPluginTest.php
│       ├── BlockSuspiciousMediaAppPathPluginTest.php
│       ├── BlockSuspiciousMediaPathPluginTest.php
│       ├── HardenImageContentValidatorPluginTest.php
│       ├── HardenImageProcessorPluginTest.php
│       ├── ValidateCustomOptionUploadPluginTest.php
│       ├── ValidateUploadedFileContentPluginTest.php
│       └── ValidateUploadedFileNamePluginTest.php
└── registration.php

Logging

All security events are written to var/log/polyshell_security.log via a dedicated Monolog channel (polyshell_security). Log entries include:

  • Blocked upload attempts with sanitized filenames, entity types, and MIME types.
  • Blocked request paths with client IP addresses.
  • Blocked media serving attempts.
  • Polyglot content detection events.
  • Reflection failures that could degrade plugin effectiveness.

Log context values are sanitized by SecurityLogSanitizer to prevent log injection (control characters stripped, whitespace normalized, values truncated to 256 characters).

Fail-Open vs Fail-Closed Design

Plugin Failure Mode Rationale
BlockCustomerFileUploadPlugin Fail-closed If reflection cannot read entityTypeCode, the entity type is set to unknown_blocked and the upload is rejected. Overly restrictive is safer than permissive.
BlockSuspiciousMediaAppPathPlugin Fail-open If reflection on Media::$relativeFileName fails, the request passes through. Fail-closed would break ALL media serving. Other layers (nginx, BlockSuspiciousMediaPathPlugin) provide backup. Reflection failures are logged at error level.
HardenImageProcessorPlugin (uploader lock) Fail-open If the uploader reflection fails, other layers (ImageContentValidator plugin, path blocking) still enforce extension restrictions. Failure is logged.

Compatibility

  • Adobe Commerce: 2.4.8-p4 (tested), expected compatible with 2.4.7+
  • PHP: 8.4 (tested). All reflection uses ::class to access properties declared on parent classes correctly in PHP 8.4's stricter reflection model.
  • MarkShust_PolyshellPatch: Integrates and replaces. The composer.json replace directive handles automatic migration.
  • Hyva Theme: No frontend dependencies. This module operates entirely on backend API and framework interception points.

Running Tests

# From Docker environment
docker compose exec phpfpm php vendor/bin/phpunit app/code/Aregowe/PolyShellProtection/Test/Unit/

Verification After Deployment

  1. Check logs: tail -f var/log/polyshell_security.log — should show blocked attempts during testing.
  2. Test blocked upload: Use curl to attempt uploading a .php file via the guest cart API. Expect rejection.
  3. Test blocked paths: Request /media/custom_options/quote/test.php directly. Expect 404.
  4. Test legitimate uploads: Verify product image uploads via admin still work normally.
  5. Scan for existing compromise: find pub/media/custom_options -name '*.php' -o -name '*.phtml' should return no results. Also check: find . -name 'accesson.php' -type f.

Support

If this module saved your store, consider buying me a coffee ☕ on Ko-fi. Every tip is appreciated and helps fund continued security research and maintenance.