Skip to content

RedirectContinuesProtectedExecution ​

warning native on by default

Group: 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 without elseif/else whose body does not terminate and emits a literal Location: 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 $v is falsy. $v is proven truthy at the call, and nothing is reported, when the conditions guarding the call hold only if $v is truthy: an arm entered only then (if/elseif/while body, else/later elseif arm of a condition that is true whenever $v is falsy, ternary or match (true) arm, right operand of &&/and/||/or), or code after an if between the redirect branch and the call whose branch for a falsy $v always leaves, with or without other arms. Conditions are understood through parentheses, !, &&/and, ||/or and comparisons of $v with true/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 $v between 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 later if (!$v) { return; } before the call, $v ? op() : null, $v && op(), !$v || op() and the else arm of a second if (!$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

Released under the MIT License. Native rules and rules modeled on Php Inspections (EA Extended); independent clean-room implementation.