web php

Redirects

A redirect tells the client to make a new request to a different URL. In PHP, redirects are usually sent with a Location header and a 3xx status code.

Redirects are common after successful form submissions, login/logout, authorization failures, moved pages, and canonical URL handling. They must happen before body output, and user-controlled redirect targets must be treated carefully.

Basic redirect response

In plain PHP, a redirect looks like this:

PHP example
<?php

declare(strict_types=1);

http_response_code(302);
header('Location: /dashboard');
exit;

The exit matters. Without it, the script may continue running and produce a body or side effects after the redirect decision.

Common redirect status codes

Use the status code that matches the meaning:

302 Found            temporary redirect, common default after actions
303 See Other        redirect after a POST so the next request is a GET
301 Moved Permanently permanent URL change
307 Temporary Redirect preserves the original method
308 Permanent Redirect preserves the original method permanently

For most form success flows, 303 See Other is a good fit because it avoids the browser resubmitting the POST if the user refreshes the success page.

Post/Redirect/Get

After handling a successful POST, redirect to a GET page. This pattern is called Post/Redirect/Get.

PHP example
<?php

declare(strict_types=1);

function redirectAfterSave(string $path): array
{
    return [
        'status' => 303,
        'headers' => ['Location' => $path],
    ];
}

$response = redirectAfterSave('/articles/123');

echo $response['status'] . ' ' . $response['headers']['Location'] . PHP_EOL;

// Prints:
// 303 /articles/123

This command-line example returns the redirect details as an array. A real controller would convert that into an HTTP response.

Avoid open redirects

An open redirect happens when the application redirects to a user-controlled external URL without checking it. Attackers use this to make a trusted domain send users to a malicious site.

PHP example
<?php

declare(strict_types=1);

function safeReturnPath(string $value): string
{
    if (!str_starts_with($value, '/')) {
        return '/';
    }

    if (str_starts_with($value, '//')) {
        return '/';
    }

    return $value;
}

echo safeReturnPath('/account') . PHP_EOL;
echo safeReturnPath('https://evil.example') . PHP_EOL;

// Prints:
// /account
// /

For most applications, accept internal paths such as /account rather than full URLs from user input.

Validate More Than The Prefix

Checking the leading slash is a useful first rule, but production code should use one central redirect helper and test the edge cases it accepts. Reject control characters, backslashes where your server may normalise them unexpectedly, and destinations outside the intended part of the application.

PHP example
<?php

declare(strict_types=1);

function hasHeaderControlCharacters(string $value): bool
{
    return str_contains($value, "\r") || str_contains($value, "\n");
}

var_dump(hasHeaderControlCharacters("/account\nX-Test: injected"));

// Prints:
// bool(true)

Framework redirect helpers are preferable because they centralise encoding and response construction. The application still owns the allow-list decision.

Redirects Are Responses, Not Authorization

A redirect to /login does not itself protect a page. The protected route must check authentication and authorization before returning sensitive content.

Likewise, redirecting after a failed authorization check should not perform the forbidden action first. Decide access before changing state, then return the redirect response.

What you should be able to do

After this lesson, you should be able to send a redirect response, choose a sensible 3xx status code, explain Post/Redirect/Get, stop execution after redirecting, validate return destinations centrally, and avoid confusing redirects with authorization.

Practice

Task: Build A Safe Return Redirect

Create a small PHP helper that decides where to redirect after login.

Requirements

  • Accept a submitted return path.
  • Allow internal paths such as /account.
  • Reject external URLs such as https://example.com.
  • Reject protocol-relative URLs such as //example.com.
  • Return / as the fallback.
  • Show at least one allowed and two rejected examples.
  • Add a short note explaining why Post/Redirect/Get is useful after successful forms.

Check your work

The helper should prevent open redirects while still supporting normal internal return paths.

Show solution
PHP example
<?php

declare(strict_types=1);

function safeReturnPath(string $value): string
{
    if (!str_starts_with($value, '/')) {
        return '/';
    }

    if (str_starts_with($value, '//')) {
        return '/';
    }

    return $value;
}

$examples = ['/account', 'https://evil.example', '//evil.example'];

foreach ($examples as $example) {
    echo $example . ' -> ' . safeReturnPath($example) . PHP_EOL;
}

// Prints:
// /account -> /account
// https://evil.example -> /
// //evil.example -> /

Post/Redirect/Get is useful after successful forms because the browser ends on a GET request. Refreshing the success page does not resubmit the original POST, which reduces duplicate submissions and confusing browser warnings.