VNX-PHP-009 – PHP preg_replace() with /e Modifier
Overview
This rule flags calls to preg_replace() that include the /e modifier in the pattern string. The /e flag causes PHP to evaluate the replacement string as PHP code after performing the substitution — effectively turning any preg_replace() call that operates on user-controlled input into an eval() call. This modifier was deprecated in PHP 5.5.0 and entirely removed in PHP 7.0.0; code that uses it either belongs to a legacy codebase still running PHP 5 or was written without awareness of the security implications. This maps to CWE-94: Improper Control of Generation of Code (‘Code Injection’).
Severity: Critical | CWE: CWE-94 – Improper Control of Generation of Code (‘Code Injection’)
Why This Matters
The /e modifier was designed to allow dynamic replacement strings in regex substitution, but it was fundamentally unsafe because the evaluated expression is constructed from the match result before any sanitization can be applied. If an attacker controls any part of the string being searched — the subject of preg_replace() — they can inject PHP code into the replacement result, which is then executed. Unlike eval() on a variable, the code injection is indirect and easy to miss in a code review.
Consider a CMS that uses preg_replace() with /e to process template tags in user-submitted content. An attacker who can submit content containing crafted regex matches can have arbitrary PHP code evaluated by the web server process, leading to full remote code execution, data exfiltration, or a persistent web shell.
Legacy PHP 5 applications are the primary target, but /e usage also appears in code that was migrated to PHP 7 with the broken assumption that PHP silently ignores the flag (it does not — it raises a fatal error, so code using /e on PHP 7+ either never reaches this line in production, or the application is pinned to PHP 5).
What Gets Flagged
The rule matches .php files where preg_replace() is called with a pattern string that includes the e modifier flag.
// FLAGGED: /e modifier evaluates replacement as PHP code
$output = preg_replace('/\[b\](.*?)\[\/b\]/e', '"<b>".$1."</b>"', $user_input);
// FLAGGED: /ei — case-insensitive and eval
$result = preg_replace('/\{(\w+)\}/ei', '$this->$1', $template);
// FLAGGED: pattern using # delimiters with e flag
preg_replace('#\{\{(.+?)\}\}#e', '$this->render($1)', $content);
// FLAGGED: common legacy BBCode parser pattern
preg_replace("|\[code\](.+?)\[/code\]|es", "highlight_string('$1')", $post);
Remediation
- Replace
preg_replace()withpreg_replace_callback(). This is the direct, safe replacement. Instead of embedding a PHP expression in the replacement string, provide a callback function that receives the match array and returns the replacement string:
// SAFE: preg_replace_callback() — explicit callback, no eval
$output = preg_replace_callback(
'/\[b\](.*?)\[\/b\]/',
function (array $matches): string {
return '<b>' . htmlspecialchars($matches[1], ENT_QUOTES, 'UTF-8') . '</b>';
},
$user_input
);
- For template engines, use an explicit tag-to-value map. Rather than evaluating PHP expressions, look up replacement values from a predefined array:
// SAFE: map-based template substitution — no dynamic code execution
$vars = [
'username' => htmlspecialchars($user->name, ENT_QUOTES, 'UTF-8'),
'date' => date('Y-m-d'),
];
$output = preg_replace_callback(
'/\{\{(\w+)\}\}/',
function (array $m) use ($vars): string {
return $vars[$m[1]] ?? '';
},
$template
);
If the codebase still runs on PHP 5, migrate to PHP 8.x immediately. PHP 5 reached end-of-life in December 2018 and has received no security patches since. The continued use of PHP 5 means your application is exposed to hundreds of unpatched CVEs in addition to this code-level vulnerability.
Audit all
preg_replace()calls for/eusing a global search before deploying any fix:
grep -rn "preg_replace\s*(" /path/to/project --include="*.php" | grep "['\"].*\/.*e[a-z]*['\"]"
- Consider a mature template engine such as Twig or Blade for user-facing template rendering. These engines compile templates to PHP functions rather than evaluating arbitrary expressions, and they escape output by default.
References
- CWE-94: Improper Control of Generation of Code (‘Code Injection’)
- CAPEC-242: Code Injection
- MITRE ATT&CK T1059 – Command and Scripting Interpreter
- PHP manual: preg_replace() — e modifier deprecation notice
- PHP manual: preg_replace_callback()
- PHP 7.0 Migration Guide – preg_replace /e removed
- OWASP PHP Security Cheat Sheet