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.

© 2026 Xygeni. All rights reserved