{"id":49083516,"url":"https://github.com/aregowe/magento2-module-polyshell-protection","last_synced_at":"2026-04-21T15:01:05.379Z","repository":{"id":352172712,"uuid":"1210703150","full_name":"aregowe/magento2-module-polyshell-protection","owner":"aregowe","description":"Comprehensive defense-in-depth Magento 2 module that closes the PolyShell unrestricted file upload vulnerability (APSB25-94) — blocking polyglot webshell uploads across eight interception layers including request path blocking, controller-level upload prevention, polyglot file detection, and framework-level image hardening.","archived":false,"fork":false,"pushed_at":"2026-04-20T13:49:14.000Z","size":180,"stargazers_count":34,"open_issues_count":0,"forks_count":3,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-20T14:50:34.635Z","etag":null,"topics":["magento","magento2","php","php8","security"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aregowe.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-14T17:13:56.000Z","updated_at":"2026-04-20T13:49:14.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/aregowe/magento2-module-polyshell-protection","commit_stats":null,"previous_names":["aregowe/magento2-module-polyshell-protection"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/aregowe/magento2-module-polyshell-protection","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aregowe%2Fmagento2-module-polyshell-protection","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aregowe%2Fmagento2-module-polyshell-protection/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aregowe%2Fmagento2-module-polyshell-protection/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aregowe%2Fmagento2-module-polyshell-protection/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aregowe","download_url":"https://codeload.github.com/aregowe/magento2-module-polyshell-protection/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aregowe%2Fmagento2-module-polyshell-protection/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32097173,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-21T11:25:29.218Z","status":"ssl_error","status_checked_at":"2026-04-21T11:25:28.499Z","response_time":128,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["magento","magento2","php","php8","security"],"created_at":"2026-04-20T14:05:25.877Z","updated_at":"2026-04-21T15:01:05.359Z","avatar_url":"https://github.com/aregowe.png","language":"PHP","funding_links":["https://ko-fi.com/aregowe"],"categories":[],"sub_categories":[],"readme":"# Aregowe_PolyShellProtection\n\n## Purpose\n\nComprehensive 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.\n\nThis module was originally forked from [markshust/magento2-module-polyshell-patch](https://github.com/markshust/magento-polyshell-patch) by [Mark Shust](https://github.com/markshust). 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.\n\n**Reference:** [Sansec — PolyShell: unrestricted file upload in Magento and Adobe Commerce](https://sansec.io/research/magento-polyshell)\n\nIf this module helped protect your store, consider [buying me a coffee ☕](https://ko-fi.com/aregowe) — it helps me keep maintaining and improving it.\n\n## Installation\n\n### Via Composer (recommended)\n\n```bash\ncomposer require aregowe/magento2-module-polyshell-protection\nbin/magento module:enable Aregowe_PolyShellProtection\nbin/magento setup:upgrade\nbin/magento cache:flush\n```\n\n### Manually\n\nCopy the module into your project:\n\n```bash\nmkdir -p app/code/Aregowe/PolyShellProtection\ncp -r * app/code/Aregowe/PolyShellProtection/\nbin/magento module:enable Aregowe_PolyShellProtection\nbin/magento setup:upgrade\nbin/magento cache:flush\n```\n\n### Uninstallation\n\n```bash\nbin/magento module:disable Aregowe_PolyShellProtection\nbin/magento setup:upgrade\ncomposer remove aregowe/magento2-module-polyshell-protection\nbin/magento cache:flush\n```\n\n## Migrating from MarkShust_PolyshellPatch\n\nThis module **integrates and supersedes** [markshust/magento2-module-polyshell-patch](https://github.com/markshust/magento-polyshell-patch). You do **not** need both modules — this one includes all of Mark Shust's original protection and extends it significantly.\n\nMark 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:\n- Polyglot file scanning (detects valid images with embedded PHP)\n- No-extension and double-extension attack detection\n- Multi-pass URL decoding and obfuscation normalization\n- Known attack filename/pattern matching\n- Request path blocking at the FrontController and `pub/get.php` level\n- Controller-level upload blocking for customer attribute and file upload endpoints\n- Configurable filename validation for custom option file uploads via the Webapi File Processor, with admin-configurable allowed and blocked extension lists\n\nThe `composer.json` includes a `\"replace\"` directive for `markshust/magento2-module-polyshell-patch`, so Composer will automatically handle the transition.\n\n### If you currently have MarkShust_PolyshellPatch installed\n\n```bash\nbin/magento module:disable MarkShust_PolyshellPatch\nbin/magento setup:upgrade\ncomposer require aregowe/magento2-module-polyshell-protection\nbin/magento module:enable Aregowe_PolyShellProtection\nbin/magento setup:upgrade\nbin/magento cache:flush\n```\n\nComposer's `replace` directive will remove MarkShust's package automatically when this module is installed.\n\n### Credits\n\nThis module was forked from [markshust/magento2-module-polyshell-patch](https://github.com/markshust/magento-polyshell-patch), created by [Mark Shust](https://github.com/markshust) and sponsored by [M.academy](https://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!\n\n## Vulnerability Summary\n\nMagento'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.\n\nThree critical checks are missing from core Magento:\n\n1. **No option ID validation** — the submitted option ID is never verified against the product's actual options.\n2. **No option type gating** — file upload logic triggers regardless of whether the product has a file-type option.\n3. **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).\n\nThe most dangerous endpoints are the anonymous guest cart routes:\n\n| Method | Endpoint | Auth Required |\n|--------|----------|---------------|\n| POST | `/V1/guest-carts/:cartId/items` | None |\n| PUT | `/V1/guest-carts/:cartId/items/:itemId` | None |\n\n### Live Attack Patterns\n\nAttackers upload **polyglot files** — valid GIF or PNG images containing executable PHP. Two payload types are in active use:\n\n- **Cookie-authenticated webshell** — GIF89a polyglot dropped as `index.php`, verifies cookie against hardcoded MD5 hash, executes arbitrary code via `eval(base64_decode())`.\n- **Password-protected RCE shell** — uses `hash_equals()` with double-MD5 hash, passes commands to `system()`.\n\nCommon 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`.\n\nPost-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.\n\n## How This Module Protects\n\nThis 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.\n\n### Defense Layers (in execution order)\n\n#### Layer 1 — Request Path Blocking\n\n| Plugin | Target Class | Strategy |\n|--------|-------------|----------|\n| `BlockSuspiciousMediaPathPlugin` | `FrontController` | Blocks HTTP requests to `/media/customer_address/`, `/media/custom_options/`, etc. via `aroundDispatch`. Returns 404. |\n| `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. |\n\n#### Layer 2 — Controller-Level Upload Blocking\n\n| Plugin | Target Class | Strategy |\n|--------|-------------|----------|\n| `BlockCustomerAttributeFileUploadControllerPlugin` | `AbstractUploadFile` | Blocks ALL customer attribute file upload controllers at the entry point. Returns JSON error. |\n| `BlockCustomerFileUploadPlugin` | `FileProcessor` | Blocks `saveTemporaryFile` and `moveTemporaryFile` for `customer_address`, `customer_addresses`, and `custom_options` entity types. Fails closed if reflection cannot read entity type. |\n\n#### Layer 3 — Custom Option Upload Validation\n\n| Plugin | Target Class | Strategy |\n|--------|-------------|----------|\n| `ValidateUploadedFileNamePlugin` | `File\\Processor` (Webapi) | Validates the filename of custom option file uploads against the merged allowlist/blocklist via `FileUploadGuard::assertSafeFileName()`. Safe files pass through; dangerous files are blocked with a logged warning. |\n| `ValidateUploadedFileContentPlugin` | `File\\Processor` (Webapi) | Validates filename safety (extension, pattern, obfuscation) and scans file content for polyglot/embedded PHP. |\n| `ValidateCustomOptionUploadPlugin` | `CustomOptionProcessor` | Validates filenames in custom option `file_info` payloads at cart/quote level via `FileUploadGuard::assertSafeFileName()`. Iterates all custom options; cart items without file payloads pass through unmodified. |\n\n#### Layer 4 — Framework-Level Image Hardening\n\n| Plugin | Target Class | Strategy |\n|--------|-------------|----------|\n| `HardenImageContentValidatorPlugin` | `ImageContentValidator` | After core validation, enforces a strict image-only extension allowlist, infers extensions for extension-less uploads by delegating to `FileUploadGuard::inferExtensionForFileName()` (MIME type is fetched lazily only when needed), blocks uploads when the MIME type is missing or unmapped, detects double-extension attacks (`.php.jpg`), scans base64 content for polyglot payloads. Integrates MarkShust_PolyshellPatch's extension check. |\n| `HardenImageProcessorPlugin` | `ImageProcessor` | Before file write, locks the Uploader's allowed extensions via reflection, infers extensions for extension-less payloads by delegating to `FileUploadGuard::inferExtensionForFileName()` (MIME type is fetched lazily only when needed), blocks uploads with missing or unmapped MIME types and non-image extensions, scans for polyglot content. |\n\n### Security Models\n\n| Model | Responsibility |\n|-------|---------------|\n| `FileUploadGuard` | Orchestrates filename validation: configurable extension allowlist/blocklist (base code-defined sets merged with admin-configured additions via `getAllowedExtensions()` / `getBlockedExtensions()`), blocked-extension pattern matching, private multi-pass normalization (unicode decoding, URL decoding, CR/LF/TAB replacement, whitespace collapse), MIME-to-extension inference via `inferExtensionFromMimeType()`, and a combined infer-validate-normalize flow via `inferExtensionForFileName()` used by both image-hardening plugins. The blocklist always overrides all allowlists. Delegates attack-pattern and polyglot detection to AttackPatternDetector and PolyglotFileDetector. |\n| `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). |\n| `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 (`\u003c?php`, `eval(`, `system(`, `exec(`, etc.) and known attack beacon signatures (`409723*20`, campaign-specific MD5 hashes). |\n| `SecurityPathGuard` | Evaluates request paths and media-relative paths against blocked directory prefixes (`/media/customer_address`, `/media/custom_options`, etc.). |\n| `SecurityLogSanitizer` | Sanitizes log context values — strips control characters, collapses whitespace, enforces maximum length — to prevent log injection attacks. |\n\n### Additional Defenses\n\n- **Nginx deny rules** — recommended in tandem with this module to block direct access to `pub/media/custom_options/` at the web server level.\n\n## Admin Configuration\n\nThe module provides an admin panel at **Stores \u003e Configuration \u003e PolyShell Protection** for configuring file upload extension policies without code changes.\n\n### File Upload Settings\n\n| Setting | Description |\n|---------|-------------|\n| **Base Allowed Extensions (read-only note)** | Displays the code-defined base allowlist: `7z, bmp, csv, doc, docx, gif, heic, jpeg, jpg, ods, odt, pdf, png, rar, rtf, txt, webp, xls, xlsx, zip`. |\n| **Additional Allowed Extensions** | Comma-separated list of extra extensions to allow (e.g. `ai, psd, svg`). Case-insensitive. Extensions that match blocked patterns are always rejected regardless of this setting. |\n| **Base Blocked Extensions (read-only note)** | Displays the code-defined base blocklist: `asp, aspx, bat, cgi, cmd, com, dll, exe, inc, jar, js, jsp, mjs, module, msi, phar, php (incl. php3–php8), phps, pht, phtml, phtm, pl, ps1, py, sh, shtml, so, vbs`. Double-extension patterns (e.g. `file.php.jpg`) are also blocked automatically. |\n| **Additional Blocked Extensions** | Comma-separated list of extra extensions to block (e.g. `svg, swf, html`). Case-insensitive. **Overrides all allowlists** — if an extension appears here and in the allowed list, it will be blocked. |\n\n### Precedence Rules\n\n1. The **blocklist always wins**. If an extension appears in both the allowed and blocked lists, it is blocked.\n2. Admin-configured extensions are merged with base code-defined sets at runtime.\n3. Extensions matching `BLOCKED_EXTENSION_PATTERN` (executable/script patterns and double-extension attacks) are **always** rejected, even if added to the allowed list via admin.\n\n### ACL\n\nAccess to the configuration section requires the `Aregowe_PolyShellProtection::config` ACL resource, nested under **Stores \u003e Settings \u003e Configuration**.\n\n## Module Structure\n\n```\napp/code/Aregowe/PolyShellProtection/\n├── etc/\n│   ├── module.xml                     # Module declaration\n│   ├── di.xml                         # Plugin wiring and DI configuration\n│   ├── acl.xml                        # ACL resource for admin config access\n│   ├── config.xml                     # Default configuration values\n│   └── adminhtml/\n│       └── system.xml                 # Admin UI: Stores \u003e Config \u003e PolyShell Protection\n├── Logger/\n│   ├── Logger.php                     # Dedicated Monolog logger channel\n│   └── Handler/\n│       └── SecurityHandler.php        # Writes to var/log/polyshell_security.log\n├── Model/\n│   ├── AttackPatternDetector.php      # Known attack filenames and regex patterns\n│   ├── FileUploadGuard.php            # Orchestrator: extension, pattern, content checks\n│   ├── PolyglotFileDetector.php       # Image signature + embedded PHP detection\n│   ├── SecurityLogSanitizer.php       # Log context sanitization\n│   └── SecurityPathGuard.php          # Request path blocking rules\n├── Plugin/\n│   ├── BlockCustomerAttributeFileUploadControllerPlugin.php\n│   ├── BlockCustomerFileUploadPlugin.php\n│   ├── BlockSuspiciousMediaAppPathPlugin.php\n│   ├── BlockSuspiciousMediaPathPlugin.php\n│   ├── HardenImageContentValidatorPlugin.php\n│   ├── HardenImageProcessorPlugin.php\n│   ├── ValidateCustomOptionUploadPlugin.php\n│   ├── ValidateUploadedFileContentPlugin.php\n│   └── ValidateUploadedFileNamePlugin.php\n├── Test/Unit/\n│   ├── MimeExtensionInferenceValidationTest.php\n│   ├── Model/\n│   │   ├── AttackPatternDetectorTest.php\n│   │   ├── FileUploadGuardTest.php\n│   │   ├── PolyglotFileDetectorTest.php\n│   │   ├── SecurityLogSanitizerTest.php\n│   │   └── SecurityPathGuardTest.php\n│   └── Plugin/\n│       ├── BlockCustomerAttributeFileUploadControllerPluginTest.php\n│       ├── BlockCustomerFileUploadPluginTest.php\n│       ├── BlockSuspiciousMediaAppPathPluginTest.php\n│       ├── BlockSuspiciousMediaPathPluginTest.php\n│       ├── HardenImageContentValidatorPluginTest.php\n│       ├── HardenImageProcessorPluginTest.php\n│       ├── ValidateCustomOptionUploadPluginTest.php\n│       ├── ValidateUploadedFileContentPluginTest.php\n│       └── ValidateUploadedFileNamePluginTest.php\n└── registration.php\n```\n\n## Logging\n\nAll security events are written to `var/log/polyshell_security.log` via a dedicated Monolog channel (`polyshell_security`). Log entries include:\n\n- Blocked upload attempts with sanitized filenames, entity types, and MIME types.\n- Blocked request paths with client IP addresses.\n- Blocked media serving attempts.\n- Polyglot content detection events.\n- Reflection failures that could degrade plugin effectiveness.\n\nLog context values are sanitized by `SecurityLogSanitizer` to prevent log injection (control characters stripped, whitespace normalized, values truncated to 256 characters).\n\n## Fail-Open vs Fail-Closed Design\n\n| Plugin | Failure Mode | Rationale |\n|--------|-------------|-----------|\n| `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. |\n| `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. |\n| `HardenImageProcessorPlugin` (uploader lock) | **Fail-open** | If the uploader reflection fails, other layers (ImageContentValidator plugin, path blocking) still enforce extension restrictions. Failure is logged. |\n\n## Compatibility\n\n- **Adobe Commerce**: 2.4.8-p4 (tested), expected compatible with 2.4.7+\n- **PHP**: 8.4 (tested). All reflection uses `::class` to access properties declared on parent classes correctly in PHP 8.4's stricter reflection model.\n- **MarkShust_PolyshellPatch**: Integrates and replaces. The `composer.json` `replace` directive handles automatic migration.\n- **Hyva Theme**: No frontend dependencies. This module operates entirely on backend API and framework interception points.\n\n## Running Tests\n\n```bash\n# From Docker environment\ndocker compose exec phpfpm php vendor/bin/phpunit app/code/Aregowe/PolyShellProtection/Test/Unit/\n```\n\n## Verification After Deployment\n\n1. **Check logs**: `tail -f var/log/polyshell_security.log` — should show blocked attempts during testing.\n2. **Test blocked upload**: Use curl to attempt uploading a `.php` file via the guest cart API. Expect rejection.\n3. **Test blocked paths**: Request `/media/custom_options/quote/test.php` directly. Expect 404.\n4. **Test legitimate uploads**: Verify product image uploads via admin still work normally.\n5. **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`.\n\n## Support\n\nIf this module saved your store, consider [buying me a coffee ☕ on Ko-fi](https://ko-fi.com/aregowe). Every tip is appreciated and helps fund continued security research and maintenance.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faregowe%2Fmagento2-module-polyshell-protection","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faregowe%2Fmagento2-module-polyshell-protection","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faregowe%2Fmagento2-module-polyshell-protection/lists"}