DownloadCritical Bug #3: Missing exit After Header Redirect
The Mystery
Date: October 16, 2025 - 18:48
The Confusing Log
[18:48:44] DEBUG Auth Controller - Redirecting to intended: /upMVC/moda ?
[18:48:44] DEBUG Router - reqRoute: / ? WTF?!
The controller says "I'm redirecting to /upMVC/moda" but the user ends up at home /!
Root Cause: PHP Doesn't Stop After header()
The Misconception
Many developers think this stops execution: header("Location: /somewhere");
// Code continues executing! ?
The Reality
header() only QUEUES a header - it doesn't stop PHP execution!
header("Location: /upMVC/moda");
// PHP keeps running...
// More code executes...
// More headers can be sent...
header("Location: /"); // This OVERWRITES the first one! ?
The Bug in Our Code
The Flow
// User POSTs login form
display()
? case "/auth": $this->auth()
? auth() checks: logged === true? NO
? Calls $this->login()
? login() validates credentials
? Sets $_SESSION['logged'] = true
? Sends header("Location: /upMVC/moda")
? ? Returns to auth() ? BUG!
? auth() continues executing ?
? Now logged === true!
? Sends header("Location: /") ? OVERWRITES!
Visual Representation
auth() method
?
?? if (logged === true) {
? header("Location: /"); ? SECOND REDIRECT (overwrites!)
? }
? else {
? ?? login() {
? ? Set logged = true
? ? header("Location: /upMVC/moda"); ? FIRST REDIRECT
? ? return; ? Goes back to auth()!
? ? }
? ?? // auth() continues here! ?
? }
?? // End of auth()
Why This Happens
Multiple Header() Calls
PHP allows multiple header() calls. The last one wins:
header("Location: /first"); // Queued
header("Location: /second"); // Queued (overwrites first)
header("Location: /third"); // Queued (overwrites second)
// When response is sent, only /third is used!
Our Specific Case
-
First redirect in `login()`: `header("Location: /upMVC/moda")`
-
`login()` returns
-
`auth()` continues executing
-
Now `$_SESSION['logged'] === true`
-
Second redirect in `auth()`: `header("Location: /")`
-
Second one overwrites the first! ?
The Fix
Add exit after EVERY header("Location: ..."):
Fix #1: In auth() method
private function auth()
{
if (isset($_SESSION["logged"]) && $_SESSION["logged"] === true) {
$intendedUrl = $_SESSION['intended_url'] ?? null;
if ($intendedUrl) {
unset($_SESSION['intended_url']);
header("Location: $intendedUrl");
exit; // ? STOP HERE!
} else {
$this->url = BASE_URL;
header("Location: $this->url");
exit; // ? STOP HERE!
}
} else {
$this->login();
}
}
Fix #2: In login() method
private function login()
{
// ... login logic ...
if ($loginSuccessful) {
$_SESSION['logged'] = true;
header("Location: " . $redirectUrl);
exit; // ? STOP HERE!
}
}
Why exit is Critical
Without exit
header("Location: /target");
// Code keeps running...
echo "Some output"; // Might prevent redirect!
header("X-Custom: value"); // Can still modify headers!
header("Location: /other"); // Can overwrite redirect!
With exit
header("Location: /target");
exit; // ? Stops ALL further execution
// Nothing below this line will execute
Testing the Fix
Before Fix
[18:48:44] Redirecting to intended: /upMVC/moda
[18:48:44] reqRoute: / ? Wrong!
After Fix
[timestamp] Redirecting to intended: /upMVC/moda
[timestamp] reqRoute: /moda ? Correct! ?
PHP Best Practices
Always Use exit After Redirects
// ? BAD
header("Location: /somewhere");
// ? GOOD
header("Location: /somewhere");
exit;
// ? ALSO GOOD
header("Location: /somewhere");
die(); // die() is alias for exit()
Or Use a Helper Function
function redirect(string $url): void
{
header("Location: $url");
exit;
}
// Usage:
redirect('/upMVC/moda'); // Automatically exits
Security Implications
Without exit, sensitive code might execute:
if (!$authenticated) {
header("Location: /login");
// ? Missing exit!
}
// This code WILL execute even for non-authenticated users! ?
$secretData = getConfidentialData();
echo json_encode($secretData);
With exit, code is safe:
if (!$authenticated) {
header("Location: /login");
exit; // ? Stops here
}
// This code will NOT execute for non-authenticated users ?
$secretData = getConfidentialData();
echo json_encode($secretData);
Summary of All 3 Bugs
Bug #1: AuthMiddleware Overwriting Session
Problem: Middleware overwrote intended_url on every request
Fix: Only store if not already set
Bug #2: Assignment Instead of Comparison
Problem: if ($logged = true) always true
Fix: Use if ($logged === true)
Bug #3: Missing exit After Redirect (THIS BUG)
Problem: Code continues after header(), sending multiple redirects
Fix: Add exit after every header("Location: ...")
Files Changed
-
`d:\GitHub\upMVC\modules\auth\Controller.php`
- Added `exit` after header in `auth()` method (2 places)
- Added `exit` after header in `login()` method (1 place)
The Lesson
> header() does NOT stop execution. Always use exit or die() after redirect headers!
This is one of the most common bugs in PHP applications and a frequent source of security vulnerabilities.
|