Skip to content
Pro Analyzer — Available with ShieldCI Pro

Open Redirection Analyzer

Analyzer IDCategorySeverityTime To Fix
open-redirection🛡️ SecurityHigh10 minutes

What This Checks

This analyzer detects open redirection vulnerabilities where user-controlled input determines redirect destinations, allowing attackers to redirect users to malicious websites. Checks for:

  • redirect() helpers with user input - redirect($input), redirect()->to(), ->away(), ->guest(), ->secure(), and the Redirect facade equivalents (Redirect::to(), ::away(), ::guest(), ::secure()) when the URL argument is user-supplied or a tainted variable
  • Raw header('Location:') calls with user input - flagged as Critical; bypasses Laravel's response pipeline entirely
  • new RedirectResponse() constructed directly with user input (both short and fully-qualified Symfony/Illuminate forms)
  • redirect()->intended() with a user-controlled fallback argument instead of a hardcoded route
  • Referer-based redirects - $_SERVER['HTTP_REFERER'], $request->header('Referer'), or $request->headers->get('referer') used as redirect targets
  • url()->previous() / URL::previous() in redirect context - both helpers derive their value from the Referer header, not session history, making them attacker-influenced

All checks also perform simple variable taint tracking (e.g. $url = $request->input('next'); redirect($url)) and recognise the following as safe:

  • Validation guardsStr::startsWith($url, '/'), parse_url($url, PHP_URL_HOST) comparisons, in_array domain allowlists, or a validate() call with a starts_with: rule within 8 lines of the redirect
  • $this->method() as redirect targetredirect($this->buildUrl($input)) is not flagged because the redirect URL is the method's return value, not the tainted argument. The method on the same controller is treated as a trusted computation boundary (e.g. it always returns a route() call)

Why It Matters

Open redirection vulnerabilities allow attackers to abuse your application's trust to redirect users to malicious destinations:

  • Phishing Attacks - Redirect users to fake login pages that steal credentials (e.g., yourapp.com/login?next=evil.com/login)
  • Malware Distribution - Send users to sites that deliver malware through drive-by downloads
  • Security Filter Bypass - Circumvent URL-based security filters and firewalls
  • Social Engineering - Exploit user trust in your domain for convincing scam campaigns
  • OAuth Token Theft - Steal authorization codes by redirecting OAuth callbacks
  • SEO Spam - Abuse your domain authority to redirect search engines to spam sites

Because the initial URL appears legitimate (it starts with your domain), users and email filters are more likely to trust and click the link.

How to Fix

Quick Fix (5 minutes)

Use named routes instead of user-supplied URLs:

Before (❌):

php
public function login(Request $request)
{
    // Authenticate user...

    // VULNERABLE: Attacker sends ?redirect=https://evil.com/steal-cookies
    return redirect($request->input('redirect'));
}

After (✅):

php
public function login(Request $request)
{
    // Authenticate user...

    // SAFE: Always redirect to a named route
    return redirect()->route('dashboard');
}

Proper Fix (10 minutes)

Validate redirect URLs against allowed domains:

Before (❌):

php
public function callback(Request $request)
{
    $returnUrl = $request->input('return_url');

    // VULNERABLE: No validation on redirect target
    return redirect()->to($returnUrl);
}

After (✅):

php
public function callback(Request $request)
{
    $returnUrl = $request->input('return_url', '/');

    // SAFE: Validate URL is internal (same domain)
    if (!$this->isInternalUrl($returnUrl)) {
        return redirect()->route('home');
    }

    return redirect()->to($returnUrl);
}

private function isInternalUrl(string $url): bool
{
    // Only allow relative URLs or same-domain URLs
    if (str_starts_with($url, '/') && !str_starts_with($url, '//')) {
        return true;
    }

    $parsed = parse_url($url);
    $appHost = parse_url(config('app.url'), PHP_URL_HOST);

    return isset($parsed['host']) && $parsed['host'] === $appHost;
}

Replace raw Location headers with Laravel redirects:

Before (❌):

php
public function legacyRedirect(Request $request)
{
    $url = $request->input('url');

    // VULNERABLE: Raw header with user input
    header('Location: ' . $url);
    exit;
}

After (✅):

php
public function legacyRedirect(Request $request)
{
    $url = $request->input('url', '/');

    // SAFE: Validate and use Laravel's redirect
    if (!$this->isInternalUrl($url)) {
        return redirect()->route('home');
    }

    return redirect($url);
}

Best Practice: Whitelist Approach with Signed URLs (✅✅):

php
use Illuminate\Support\Facades\URL;

class RedirectController extends Controller
{
    // Generate signed redirect URLs
    public function generateLink(string $destination): string
    {
        $allowedDestinations = [
            'dashboard' => route('dashboard'),
            'profile' => route('profile'),
            'settings' => route('settings'),
        ];

        if (!isset($allowedDestinations[$destination])) {
            abort(400, 'Invalid destination');
        }

        // Signed URL prevents tampering
        return URL::signedRoute('safe-redirect', [
            'destination' => $destination,
        ]);
    }

    // Handle signed redirect
    public function handleRedirect(Request $request, string $destination)
    {
        // Laravel automatically validates the signature
        if (!$request->hasValidSignature()) {
            abort(403);
        }

        $allowedDestinations = [
            'dashboard' => route('dashboard'),
            'profile' => route('profile'),
            'settings' => route('settings'),
        ];

        $url = $allowedDestinations[$destination] ?? route('home');

        return redirect($url);
    }
}

Secure intended() fallback:

Before (❌):

php
public function postLogin(Request $request)
{
    // VULNERABLE: User-controlled fallback
    return redirect()->intended($request->input('fallback'));
}

After (✅):

php
public function postLogin(Request $request)
{
    // SAFE: Hardcoded fallback route
    return redirect()->intended(route('dashboard'));
}

Replace new RedirectResponse() with validated redirect:

Before (❌):

php
use Illuminate\Http\RedirectResponse;

public function go(Request $request): RedirectResponse
{
    // VULNERABLE: Direct constructor with user input
    return new RedirectResponse($request->input('url'));
}

After (✅):

php
public function go(Request $request): RedirectResponse
{
    $url = $request->input('url', '/');

    if (!$this->isInternalUrl($url)) {
        return redirect()->route('home');
    }

    // SAFE: use redirect() helper with validated URL
    return redirect($url);
}

Avoid redirecting to the Referer header:

Before (❌):

php
public function back(Request $request)
{
    // VULNERABLE: Referer is attacker-controlled via a crafted link
    return redirect($_SERVER['HTTP_REFERER']);
}

After (✅):

php
public function back(Request $request)
{
    // SAFE: Laravel's back() validates against your session history
    return redirect()->back();

    // OR if you must use Referer, validate it:
    // $referer = $request->header('Referer', '/');
    // return $this->isInternalUrl($referer) ? redirect($referer) : redirect('/');
}

Replace url()->previous() with redirect()->back():

Before (❌):

php
public function cancel()
{
    // VULNERABLE: url()->previous() reads the Referer header directly
    return redirect(url()->previous());
}

After (✅):

php
public function cancel()
{
    // SAFE: redirect()->back() uses the session-stored previous URL
    return redirect()->back();
}

References