Skip to content

VNX-PHP-009: PHP preg_replace with /e modifier

Detects preg_replace() and preg_filter() calls whose pattern literal (inline, split across lines, in an array of patterns, or in a variable assigned just above) carries the e modifier, which evaluates the replacement as PHP code. preg_replace_callback(), patterns without e and the call inside a comment or string are not reported.

Updated 02 Nov 2026Checked against cli@de9b52a
Field Value
Rule ID VNX-PHP-009
Severity Critical
SARIF level error
Kind sast
Languages PHP
CWE CWE-94
CAPEC CAPEC-242
ATT&CK T1059
CWSS vector CWSS:1.0/TI:C/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:C/DI:H/EX:H/EC:N/P:W
Tags rce code-injection deprecated php
Source internal/sast/rules/vnx-php-009.rego

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’).

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() or preg_filter() is given a pattern literal whose closing delimiter (any delimiter, including {} pairs) is followed only by valid PCRE modifiers that include e. The pattern may be inline, on the next lines of a call broken across lines, in an array of patterns, or in a variable assigned within the 15 lines above. Not flagged: preg_replace_callback(), patterns such as '/edit/' whose trailing text is not a modifier list, and the call inside a comment or string literal.

// FLAGGED: the pattern comes from a variable assigned just above
$pattern = '/:(\w+):/e';
return preg_replace($pattern, 'smiley_html("$1")', $text);

// 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

  1. Replace preg_replace() with preg_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
);
  1. 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
);
  1. 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.

  2. Audit all preg_replace() calls for /e using a global search before deploying any fix:

grep -rn "preg_replace\s*(" /path/to/project --include="*.php" | grep "['\"].*\/.*e[a-z]*['\"]"
  1. 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

Suppressing this rule

Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):

# nosec vnx-php-009
// nosec vnx-php-009

Or record an ignore rule: vulnetix ignore add --rule vnx-php-009 --reason "...". See Suppressing findings.

Edit this pageSources: cli/internal/sast/rules/vnx-php-009.rego, cli/internal/sast/docs/vnx-php-009.md
esc
↑ ↓ move↵ openSearches in your browser. A key like autonomy.max or a code like threshold_required jumps straight to it.