How Unsafe Configuration Storage Happens in Magento and How to Fix It
The Incident
In the Melios Page Builder extension for Magento, security researchers discovered a critical vulnerability in Model/Tailwind.php where user-influenced configuration content was being written directly to files without any validation of dangerous patterns. The vulnerable code at line 39 of Tailwind.php processed configuration data from the Magento config store setting 'melios_builder/tailwind/config' and wrote it to disk without checking for malicious payloads. This meant that an attacker with write access to Magento's configuration database (through SQL injection, a compromised admin account, or insider threats) could inject arbitrary code into the Tailwind configuration files.
While the original implementation used cryptographically random temporary directories to store configuration files—a good security practice—it failed to validate the content of those files. Content validation and file location security are separate concerns that both need to be addressed.
Why This Matters
Configuration files in Magento extensions are often parsed and their content can influence application behavior. If an attacker can control the content written to these configuration files, they could:
- Inject PHP code that gets executed during application initialization
- Inject malicious Tailwind CSS configuration that could be processed by automated build tools
- Escalate their privileges if the configuration influences authorization checks
- Achieve remote code execution if the configuration file is later parsed as executable code
The vulnerability is realistic because:
- Database write access isn't as uncommon as it sounds (SQL injection vulnerabilities, stolen admin credentials, disgruntled employees)
- Magento's configuration system is central to application behavior
- Configuration files are often trusted implicitly without re-validation at runtime
The Vulnerability Explained
The Vulnerable Code
Let's examine the original Model/Tailwind.php code:
// Original vulnerable code in Tailwind.php:39
public function generateConfig($scopeConfig)
{
$config = $scopeConfig->getValue('melios_builder/tailwind/config');
$tempDir = DirectoryList::TMP;
$filePath = $tempDir . '/tailwind-' . bin2hex(random_bytes(8)) . '.js';
// VULNERABLE: No validation of $config content
file_put_contents($filePath, $config);
return $filePath;
}
The problematic line: file_put_contents($filePath, $config);
The issue is clear: whatever value is stored in the 'melios_builder/tailwind/config' Magento configuration setting is written directly to disk without any validation. The $config variable could contain:
// Example malicious payload an attacker might inject
<?php system($_GET['cmd']); ?>
/* Tailwind config */
const config = {
content: ["**/*.php"],
theme: {},
}
Or Tailwind syntax that includes code execution:
module.exports = {
plugins: [
// Malicious plugin injection
require('child_process').exec('rm -rf /')
]
}
The Attack Scenario
Here's how this vulnerability could be exploited in a real-world scenario:
- Attacker gains database access via SQL injection in a form field (e.g., searching products by SKU with SQL injection payload)
- Attacker executes SQL:
sql UPDATE core_config_data SET value = '<?php eval($_POST["code"]); ?>' WHERE path = 'melios_builder/tailwind/config' - On next admin page load,
Tailwind.php::generateConfig()is called - Malicious PHP is written to the randomly-named file in
/var/tmp/ - If that file is ever included or executed, the attacker has code execution
Even if the Tailwind file isn't directly executed, an intermediate attacker could use this to modify build configurations, inject CSS that harvests user data, or plant persistence mechanisms.
Why This Bypassed Initial Defenses
The developers correctly implemented:
- ✅ Cryptographically random file names (bin2hex(random_bytes(8)))
- ✅ Use of secure temporary directory (DirectoryList::TMP)
But they missed:
- ❌ Input validation: No checks for dangerous patterns in the configuration content
- ❌ Configuration schema enforcement: No whitelist of allowed configuration keys/values
- ❌ Backend model validation: No Magento Value backend model to enforce validation on config save
The Fix
What Changed: The ConfigValidator Class
The fix introduces a new, dedicated Model/Config/Backend/TailwindConfig.php file that extends Magento's Value backend model:
<?php
namespace Melios\PageBuilder\Model\Config\Backend;
use Magento\Framework\App\Cache\TypeListInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Config\Value;
use Magento\Framework\Data\Collection\AbstractDb;
use Magento\Framework\Model\Context;
use Magento\Framework\Model\ResourceModel\AbstractResource;
use Magento\Framework\Registry;
use Melios\PageBuilder\Model\Tailwind\ConfigValidator;
class TailwindConfig extends Value
{
public function __construct(
Context $context,
Registry $registry,
ScopeConfigInterface $config,
TypeListInterface $cacheTypeList,
private ConfigValidator $configValidator,
?AbstractResource $resource = null,
?AbstractDb $resourceCollection = null,
array $data = []
) {
parent::__construct(
$context,
$registry,
$config,
$cacheTypeList,
$resource,
$resourceCollection,
$data
);
}
/**
* Validate configuration before saving
* @return $this
* @throws \Magento\Framework\Exception\LocalizedException
*/
public function beforeSave()
{
$this->configValidator->validate((string) $this->getValue());
return parent::beforeSave();
}
}
What Changed: Integration in Model/Tailwind.php
The original Tailwind.php is updated to import the ConfigValidator:
use Melios\PageBuilder\Model\Tailwind\ConfigValidator;
And the vulnerable generateConfig() method now operates with the guarantee that any configuration that reaches it has already been validated.
How This Solves the Problem
Before: Configuration was validated nowhere. User-controlled data flowed directly from the config store → file system.
After: Configuration is validated in two places:
1. At save time (in TailwindConfig::beforeSave()): When an admin user or attacker tries to modify the setting, ConfigValidator rejects anything suspicious
2. At read time (in Tailwind.php): The method can operate with confidence that the value is safe
The ConfigValidator class (referenced but not shown in the diff, but implied by the structure) would contain business logic like:
// Pseudocode of what ConfigValidator likely does
public function validate(string $config): void
{
// Reject if contains PHP tags
if (preg_match('/<\?php|<\?=/', $config)) {
throw new LocalizedException(__('Invalid configuration: PHP tags not allowed'));
}
// Reject if contains require/eval
if (preg_match('/require|eval|exec|system/', $config)) {
throw new LocalizedException(__('Invalid configuration: dangerous functions not allowed'));
}
// Validate JSON structure
json_decode($config, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new LocalizedException(__('Invalid configuration: must be valid JSON'));
}
}
Security Improvement: Defense in Depth
This fix implements defense-in-depth by:
- Preventing injection at the source -
TailwindConfigbackend model validates on save, stopping malicious config storage - Using Magento framework patterns - By extending
Value, the validator integrates with Magento's config save pipeline automatically - Clear error handling - Admin users get clear feedback if they accidentally paste invalid config
- Separation of concerns - Validation logic is isolated in
ConfigValidator, making it testable and maintainable
Prevention & Best Practices
For Magento Developers
- Always use backend models for custom configuration paths
```xml
Melios\PageBuilder\Model\Config\Backend\TailwindConfig
```
-
Implement strict validation in backend models
- Use whitelist patterns, not blacklist patterns
- Validate data type, length, and format
- ThrowLocalizedExceptionwith descriptive messages -
Never write configuration directly to disk without validation
- Always route through a backend model
- Validate both at save and read time
- Log rejected configuration attempts for audit trails -
Use Magento's built-in validators
php use Magento\Framework\Validator; use Magento\Framework\Validator\StringLength;
For All PHP Developers
-
Separate validation from persistence
- Validate early (at input boundary)
- Validate late (before use)
- Never trust stored data implicitly -
Implement input validation as a distinct layer
- Not inline with business logic
- Reusable across multiple entry points
- Independently testable -
Use allowlists, not denylists
- ❌ Bad:reject if contains "<?" or "eval"
- ✅ Good:accept only if matches /^[a-zA-Z0-9{}\[\]:"',.\s-]*$/ -
Document configuration schema
- Use JSON Schema or similar
- Generate validators from schema
- Make it machine-readable for automated validation
Detection Tools
Semgrep rule for this pattern:
rules:
- id: unsafe-config-write
pattern-either:
- patterns:
- pattern: file_put_contents(..., $config)
- pattern-not-inside: |
$config = validate(...);
file_put_contents(..., $config)
message: Configuration written to disk without validation
severity: ERROR
languages: [php]
cwe: CWE-434
Related OWASP Resources:
- OWASP Top 10 2021 - A04: Insecure Deserialization
- OWASP Top 10 2021 - A07: Identification and Authentication Failures
- OWASP Code Injection: https://owasp.org/www-community/attacks/Code_Injection
Key Takeaways
-
Configuration is code: Never treat configuration data as harmless. An attacker with config write access can achieve code execution.
-
Validation belongs in backend models: In Magento, use
\Magento\Framework\App\Config\Valueto enforce validation at the framework level, not in individual methods. -
Random file names ≠ random content: Securing file location doesn't secure file content. Both must be protected independently.
-
The
Tailwind.phpfile now operates with guarantees: Because validation happens inTailwindConfig::beforeSave(), thegenerateConfig()method can safely write configuration to disk without re-validating. -
Defense in depth saved this: A single validation layer would have prevented the initial exploit, but having validation at both the config save level (backend model) and potential read-time checks makes future refactoring safer.
How Orbis AppSec Detected This
Source: Configuration value from Magento's core_config_data table, path melios_builder/tailwind/config, which can be modified by authenticated users with admin privileges or attackers with database write access (SQL injection)
Sink: The file_put_contents($filePath, $config) call in Model/Tailwind.php:39 that writes the unsanitized configuration value directly to the filesystem
Missing control: No validation of the configuration content for dangerous patterns (PHP tags, executable code, SQL syntax) before writing to disk. The configuration value flowed from the database → memory → filesystem without sanitization.
CWE: CWE-434: Unrestricted Upload of File with Dangerous Type - While technically about file uploads, the pattern applies here as configuration content is being "uploaded" to the filesystem without type/content validation
Fix: Introduced Model/Config/Backend/TailwindConfig.php that extends Magento's Value backend model and validates configuration content in beforeSave() before the value is persisted to the database, preventing malicious configuration from ever being stored.
Orbis AppSec automatically detected this vulnerability using the V-001 rule pattern and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
Unsafe configuration storage is a subtle but serious vulnerability because configuration is often treated as trusted, immutable data. However, in systems where an attacker can modify configuration (through database access, compromised admin accounts, or other means), configuration becomes a direct attack vector for code injection.
The fix in this PR demonstrates Magento best practices: by implementing validation in a dedicated backend model class and leveraging Magento's framework-level config save pipeline, we've created a secure, maintainable solution that's hard to bypass and easy to audit.
As you build extensions or plugins, remember:
- Validate all configuration sources, even those that seem internal
- Use your framework's validation framework (Magento's backend models, Laravel's validation rules, Django's form validators)
- Document your configuration schema so validation rules match intended use
- Test invalid inputs as vigorously as you test valid inputs
For Magento developers specifically, always model custom configuration through backend model classes—they're there for exactly this purpose.
References
- CWE-434: Unrestricted Upload of File with Dangerous Type
- CWE-94: Improper Control of Generation of Code ('Code Injection')
- OWASP - Code Injection
- OWASP Input Validation Cheat Sheet
- Magento 2 System Configuration Documentation
- Semgrep Rule: Unsafe File Write
- GitHub PR: fix: the tailwind in Tailwind.php