Anchor Precedence In Alternation
ID |
php.regex_anchor_precedence |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Php |
Tags |
regex, reliability |
Description
Reports an alternation whose first branch starts with ^ (or \A) or whose last branch ends with $ (or \Z, \z), while at least one other branch of the same alternation carries no such anchor.
Rationale
An anchor binds tighter than |. So /^GET|POST|PUT$/ is not "one of these three, anchored at both ends" — PCRE reads it as /(?:^GET)|POST|(?:PUT$)/:
// Bad
preg_match('/^GET|POST|PUT$/', $method);
preg_match('/^GET|POST|PUT$/', 'DELETE-POST-NOW'); // 1 -- "POST" matched mid-string
-
The middle branch is anchored at neither end, so it matches anywhere in the subject.
-
For a validation pattern that means it accepts exactly the input it was written to reject, and the failure surfaces downstream as bad data rather than as a validation error.
-
The layout reads as if the anchors applied to the whole list, which is why the mistake survives review.
Remediation
Wrap the alternation in a group so the anchors apply to all of it.
// Good
preg_match('/^(GET|POST|PUT)$/', $method);
preg_match('/^(GET|POST|PUT)$/', 'DELETE-POST-NOW'); // 0
What is not reported
Two shapes look like the defect and are not:
-
A branch consisting of nothing but the anchor —
/(?:,|$)/,/(|[\\])/. That is the deliberate "either this, or the boundary" idiom: the branch has no content to anchor. -
The two-branch strip shape
/^A|B$/, as inpreg_replace('/^,|,$/', '', $s). Each branch is already anchored at the edge it is meant to strip.
Every alternation is judged at its own level — the specific group, or the top level, that directly contains it — so a nested alternation inside a properly wrapped group never inherits the anchors around it. A pattern carrying the x modifier is not analysed: under it whitespace and # comments stop being pattern content, so branch spans no longer measure what PCRE sees.