preg_replace /e Modifier (Removed)
debt(d5/e3/b3/t7)
Closest to 'specialist tool catches it' (d5), semgrep and phpstan rules flag the /e modifier pattern reliably; also PHP 7+ removes it entirely making it a fatal error there.
Closest to 'simple parameterised fix' (e3), quick_fix is to swap preg_replace with /e for preg_replace_callback — a localized pattern replacement per call site, though it requires auditing all occurrences.
Closest to 'localised tax' (b3), affects only the regex call sites in legacy code; not architectural but requires ongoing audit across the codebase wherever preg_replace is used.
Closest to 'serious trap' (t7), misconception states devs think /e evaluates simple expressions but it executes arbitrary PHP code including system()/exec() — the modifier letter gives no hint of code execution semantics.
TL;DR
Explanation
preg_replace($pattern . 'e', $replacement, $subject) evaluated $replacement as PHP code after substitution. This allowed: preg_replace('/.*/e', $_GET['cmd'], '') — direct remote code execution from user input. Removed in PHP 7.0 (was deprecated in PHP 5.5). The replacement is preg_replace_callback() with an explicit closure. Any legacy codebase running on PHP 5 with user input touching preg_replace with the /e flag has a critical RCE. Check all preg_replace calls for the 'e' flag in regex patterns.
Common Misconception
Why It Matters
Common Mistakes
- Any use of preg_replace with /e flag from user input.
- Not auditing all preg_replace calls in legacy codebases.
- Using variable patterns: preg_replace($userPattern . 'e', ...).
Code Examples
// PHP 5 — critical RCE:
preg_replace('/' . $_GET['pattern'] . '/e',
$_GET['replacement'],
$subject);
// Attacker: ?pattern=.*&replacement=system('cat /etc/passwd')
// PHP 7+ — use preg_replace_callback:
$result = preg_replace_callback(
'/([a-z]+)/',
function(array $matches): string {
return strtoupper($matches[1]);
},
$subject
);
// Never pass user input as the callback