RedirectContinuesProtectedExecution
warning native on by defaultGroup: Probable bugs
Detect a conditional unauthorized branch emitting a Location header and falling through to an operation marked protected by the same authorization predicate.
Example
php
<?php
/** @custos-protected */ function deleteRecord() {}
function bad(bool $allowed) { if (!$allowed) { header('Location: /signin'); } deleteRecord(); }Reported:
- line 3: Terminate the unauthorized branch after redirecting.
Detection
- Report a conditional unauthorized branch emitting a Location header and falling through to an operation marked protected by the same authorization predicate.
- A protected operation must be a resolved project callable explicitly documented with
@custos-protected; callable names alone never establish protection. The redirect branch tests the same authorization value guarding access to that operation. - 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.
- The redirect branch is an
if (!$v)statement withoutelseif/elsewhose body does not terminate and emits a literalLocation:header in its own scope; a header call inside a nested closure or function body does not count. The branch must precede the protected call in a statement list enclosing that call, either inside the same function-like scope or in top-level code. A protected call inside a nested closure or arrow function is not related to a redirect of the outer scope. - D6. The protected call must stay reachable while
$vis falsy.$vis proven truthy at the call, and nothing is reported, when the conditions guarding the call hold only if$vis truthy: an arm entered only then (if/elseif/whilebody,else/laterelseifarm of a condition that is true whenever$vis falsy, ternary ormatch (true)arm, right operand of&&/and/||/or), or code after anifbetween the redirect branch and the call whose branch for a falsy$valways leaves, with or without other arms. Conditions are understood through parentheses,!,&&/and,||/orand comparisons of$vwithtrue/false; a jump label between them and the call, or any condition they cannot read, proves nothing and the report is kept. - D7. Any write to
$vbetween the start of the redirect branch and the protected call (assignment, compound assignment, destructuring,unset,++/--, loop or catch binding) makes the authorization relationship unknown: no report.
Exceptions (no report)
- A terminating exit/return/throw, mutually exclusive protected branch, deliberate unconditional redirection with no protected continuation and unknown authorization relationship are excluded.
- Same-predicate guards per D6:
if (!$v) { header('Location: …'); } if ($v) { op(); }, a laterif (!$v) { return; }before the call,$v ? op() : null,$v && op(),!$v || op()and theelsearm of a secondif (!$v). - The predicate is rebound before the call (D7), the header is only emitted by a nested closure, or the protected call is only made by a nested closure or arrow function.
- 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.
Fix
No automatic fix: exit versus return depends on the containing scope and response contract.
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": {
"RedirectContinuesProtectedExecution": {
"enabled": false
}
}
}Suppress
Before the statement or declaration (or the first statement of the file):
php
// @custos-ignore RedirectContinuesProtectedExecution