Open Redirection Analyzer
| Analyzer ID | Category | Severity | Time To Fix |
|---|---|---|---|
open-redirection | 🛡️ Security | High | 10 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 theRedirectfacade 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 guards —
Str::startsWith($url, '/'),parse_url($url, PHP_URL_HOST)comparisons,in_arraydomain allowlists, or avalidate()call with astarts_with:rule within 8 lines of the redirect $this->method()as redirect target —redirect($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 aroute()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 (❌):
public function login(Request $request)
{
// Authenticate user...
// VULNERABLE: Attacker sends ?redirect=https://evil.com/steal-cookies
return redirect($request->input('redirect'));
}After (✅):
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 (❌):
public function callback(Request $request)
{
$returnUrl = $request->input('return_url');
// VULNERABLE: No validation on redirect target
return redirect()->to($returnUrl);
}After (✅):
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 (❌):
public function legacyRedirect(Request $request)
{
$url = $request->input('url');
// VULNERABLE: Raw header with user input
header('Location: ' . $url);
exit;
}After (✅):
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 (✅✅):
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 (❌):
public function postLogin(Request $request)
{
// VULNERABLE: User-controlled fallback
return redirect()->intended($request->input('fallback'));
}After (✅):
public function postLogin(Request $request)
{
// SAFE: Hardcoded fallback route
return redirect()->intended(route('dashboard'));
}Replace new RedirectResponse() with validated redirect:
Before (❌):
use Illuminate\Http\RedirectResponse;
public function go(Request $request): RedirectResponse
{
// VULNERABLE: Direct constructor with user input
return new RedirectResponse($request->input('url'));
}After (✅):
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 (❌):
public function back(Request $request)
{
// VULNERABLE: Referer is attacker-controlled via a crafted link
return redirect($_SERVER['HTTP_REFERER']);
}After (✅):
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 (❌):
public function cancel()
{
// VULNERABLE: url()->previous() reads the Referer header directly
return redirect(url()->previous());
}After (✅):
public function cancel()
{
// SAFE: redirect()->back() uses the session-stored previous URL
return redirect()->back();
}References
- OWASP Unvalidated Redirects and Forwards
- CWE-601: URL Redirection to Untrusted Site
- OWASP Top 10 - A01:2021 Broken Access Control
- Laravel Redirects Documentation
- Laravel Signed URLs Documentation
- PHP parse_url Documentation
Related Analyzers
- CSRF Protection Analyzer - Protects forms from cross-site request forgery
- XSS Vulnerabilities Analyzer - Detects cross-site scripting vulnerabilities
- Authentication Authorization Analyzer - Validates authentication patterns
- Clickjacking Analyzer - Prevents UI redress attacks