Replacement References A Missing Group
ID |
php.regex_replacement_group_reference |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Php |
Tags |
regex, reliability |
Description
Reports a preg_replace or preg_filter whose replacement string references a capture group the pattern does not declare. All three PHP spellings are read — $1, ${1} and \1.
Rationale
PHP does not fail on an out-of-range reference. It substitutes the empty string:
// Bad
preg_replace('/(\d+)-(\d+)/', '$3/$1', '12-34'); // "/12" -- group 3 became nothing
-
The call succeeds, returns a string, and the missing piece surfaces later as truncated output, a broken URL or a malformed identifier.
-
Group numbers shift whenever a group is added, removed or reordered, so the reference and the pattern drift apart in ordinary editing.
The two-digit rule
PHP’s replacement scanner takes up to two digits, greedily and unconditionally. '$12' is group 12 — never group 1 followed by a literal 2:
// Bad
preg_replace('/(\w+)/', '$12', 'abc'); // "" -- there is no group 12
// Good
preg_replace('/(\w+)/', '${1}2', 'abc'); // "abc2"
Brace the reference whenever a digit follows it. This is where PHP differs from JavaScript, which falls back to the shorter reading when the longer one does not resolve.
Remediation
Point the reference at a group the pattern declares, and brace it when a digit follows.
// Good
preg_replace('/(\d+)-(\d+)/', '$2/$1', '12-34'); // "34/12"
What is not reported
$0 is never reported: PCRE’s group 0 is the whole match. PHP has no named references in a replacement string, so there is no name form to check.
A pattern that declares no capturing group is not judged at all. That is a deliberate false negative: preg_replace('/x/', '$1,000', $s) really does produce ,000, but a currency or template string in a group-less replacement is common enough that the finding would be dominated by it. Only once the author has written a group is a reference unambiguously meant as one.
The replacement must be a single string literal; a variable, a concatenation or an interpolating literal is built at run time. preg_replace_callback and preg_replace_callback_array take a callable rather than a replacement string and are not considered. The array form is paired by position, the way PHP pairs an array of patterns with an array of replacements.