UnvalidatedRedirectTarget
error native on by defaultGroup: Security
Detect request-derived data reaching a Location header without a proven permitted-local-target or finite destination policy.
Example
php
<?php
header('Location: ' . $_GET['next']);
header("Location: {$_GET['next']}#top");
// Not reported: finite selection, and encoded query data on a fixed local path.
$page = match ($_GET['tab']) { 'billing' => '/account/billing', default => '/account' };
header('Location: ' . $page);
header('Location: /find?term=' . rawurlencode($_GET['term']));Reported:
- line 2: Validate the redirect target against the permitted destination policy.
- line 3: Validate the redirect target against the permitted destination policy.
Detection
- Report request-derived data reaching a Location header without a proven permitted-local-target or finite destination policy.
- The first argument of
header()is read as the ordered operands of its concatenation chain and of any interpolated string (double-quoted or heredoc) inside it, ignoring grouping parentheses. The leading operands whose text is statically known form the written prefix; the call is a redirect when that prefix starts withLocation:in any letter case. Every operand after the prefix is checked, so request data followed by a suffix ('Location: ' . $_GET['return'] . '?saved=1') or embedded in an interpolated string is reported exactly like a trailing operand. - A
matchexpression, written in place or reached through a variable with one agreeing definition, contributes only the request data of its arm results. The subject chooses an arm but is never part of the produced value. An arm that yields request data is still reported. Selections nested more than eight levels deep are treated as request-derived. A variable is followed to its reaching assignment only when its scope cannot rebind it behind that assignment; a reference alias, a by-reference closure capture,foreachor argument, acatchbinding orstaticdeclaration of the same name,extract(), an assignment through a variable variable, aglobaldeclaration of it or a$GLOBALSaccess that is not a different literal entry keeps the report. - Resolve builtin calls and named arguments before checking the contract. Project-local callees are considered only when their bodies or immutable summaries establish the required facts. The default is enabled, with positive proof required.
- For flow-dependent conditions, require a reachable source-to-use path with no intervening invalidation. Unknown calls, ambiguous aliases, recursion or analysis-budget exhaustion cannot establish definite facts.
Exceptions (no report)
- Literal destinations, configured allowlisted origin/path validation and unknown taint flows are excluded.
- A
matchover request data whose arms all produce values free of request data is a finite destination policy. - Fixed local path with encoded query values: when the written prefix after
Location:starts with a single/(the next character is neither/nor\) and already contains?, an operand produced by the builtinurlencode(),rawurlencode()orhttp_build_query()(directly, or through a variable with one agreeing definition) is not reported. Such a value can only be query data on the current origin. Any other request-derived operand in the same header is still reported, as is an encoded value without that prefix, in a path segment, or after a//or/\prefix. - A backtick command as the header argument is shell output, not written header text; it has no written prefix and is not reported by this rule.
- Unresolved targets, incomplete required facts, suppressed findings and PHP versions lacking the referenced API or language feature produce no report. User declarations shadowing builtin names are not builtin contracts.
- Request data that a
switchcompared with constant labels is the matched label inside an arm reached only through string labels that do not read as numbers (not empty and not starting, after whitespace, with a digit, a sign or a dot), and after the switch when every other way out replaced it or left the scope:switch ($sort) { case 'name': case 'date': break; default: $sort = 'name'; }validates$sort. A numeric, empty, boolean or null label, a label expression, a fall-through from another arm, adefaultarm and any other subject prove nothing. The subject is a plain variable or an element of a request superglobal read under one constant key (switch ($_GET['action']), not$_FILES); the element is the matched label only while nothing that may run code is evaluated: a call, an object creation, an assignment, anunset, or the use of a local variable, property, offset of a local or class constant ends it, as does a reference to the superglobal. Aswitch (true)reads each case expression like the condition of anif/elseifchain.
Fix
No automatic fix: the set of permitted destinations must be supplied by the application.
PHP versions
Apply only on PHP versions supporting the referenced APIs and syntax (within PHP 5.3–8.5). Builtin behavior and argument contracts follow the configured PHP version. Extension-specific calls require resolved extension API symbols; do not treat same-named application functions as extension calls.
Options
None. Enable or disable this rule through normal rule configuration.
Configure
In custos.json:
json
{
"rules": {
"UnvalidatedRedirectTarget": {
"enabled": false
}
}
}Suppress
Before the statement or declaration (or the first statement of the file):
php
// @custos-ignore UnvalidatedRedirectTarget