Open redirect — CWE-601 in the Common Weakness Enumeration, formally "URL Redirection to Untrusted Site" — is one of the oldest tricks in the phishing playbook, and Laravel's own helper methods make it unusually easy to reintroduce even in a framework that ships with strong defaults. The bug pattern is simple: an app reads a URL from user input, often a ?redirect= or ?next= query parameter, and sends the browser there with a 302 without checking whether that destination is actually part of the app. Attackers weaponize this by wrapping a trusted domain around a malicious one — a link to yourapp.com/login?redirect=https://evil-lookalike.com survives copy-paste inspection and email spam filters far better than a raw phishing URL, and it's a well-documented technique for stealing OAuth authorization codes and session tokens during login flows. OWASP listed unvalidated redirects and forwards as its own Top 10 category (A10) back in 2013, and while later editions folded it into broader categories rather than keeping it standalone, the underlying flaw hasn't gone anywhere — it just stopped getting its own slide. Laravel provides redirect()->intended(), redirect()->away(), and URL::previous() as ergonomic shortcuts for exactly the kind of post-login and "go back" flows where this bug likes to hide. This post walks through where each one leaks trust, and the allow-list patterns that close the gap.
What makes redirect()->intended() a common entry point?
redirect()->intended('/dashboard') is Laravel's helper for sending a user back to the page they originally tried to visit before an auth middleware intercepted them — it reads a URL stored in the session under the url.intended key and falls back to the default you pass in if nothing was stored. The vulnerability isn't in intended() itself; it's in how that session value gets populated. If a route captures a ?redirect= or ?next= query parameter and writes it into url.intended — or if a custom middleware calls redirect()->setIntendedUrl($request->query('redirect')) — without checking that the value is a same-site relative path, an attacker can craft a login link that plants an external URL, and intended() will happily send the freshly authenticated user straight to it. This is a particularly effective phishing setup because the URL looks like your own login page right up until the post-auth bounce.
Why is redirect()->away() a documented footgun?
Laravel's own documentation is explicit that redirect()->away($url) exists specifically to bypass the framework's usual URL validation and generation logic, so it can point a RedirectResponse at an external domain without Laravel rewriting it as a relative path. That's the intended behavior for genuine outbound links — sending a user to a payment provider or a third-party OAuth consent screen — but it means away() performs zero trust-checking on the string you hand it. The footgun shows up when a developer treats away() as a generic "redirect somewhere" helper and feeds it directly from $request->input('url') or a similarly named parameter, reasoning that away() "handles redirects" without registering that the method name is a signal, not a safeguard: it's telling you the check has been turned off, not that one exists.
Does Laravel's url validation rule stop open redirects?
No, and this is one of the most common misunderstandings in Laravel security reviews. The url validation rule ($request->validate(['redirect' => 'url'])) checks that a string is shaped like a valid URL — has a scheme, a host, correct syntax — it says nothing about whether that host is one your application trusts. https://totally-legit-bank.security-alert-portal.ru passes Laravel's url rule without complaint, because syntactically it's a perfectly well-formed URL. Teams that add 'redirect' => 'required|url' to a form request and consider the open-redirect risk handled have validated the wrong property: format instead of origin. The fix requires a second, separate check against an explicit allow-list of hosts or a same-origin comparison — format validation and trust validation are not the same control, and Laravel does not conflate them for you.
How does URL::previous() introduce the same risk from a different angle?
URL::previous() and the url()->previous() helper return the last known "previous" URL for the current user, and Laravel builds that value from two sources: the _previous.url key in the session, or, if that's empty, the HTTP Referer header sent by the browser. Both of those are attacker-influenceable in the right circumstances — the Referer header is trivially spoofed in a crafted request, and if an application ever writes attacker-supplied input into _previous.url (for instance, a "back" link builder that trusts a query parameter), previous() will faithfully return it later. Code like return redirect(url()->previous()); after a form submission looks like harmless UX polish, but it's functionally identical to trusting an unvalidated redirect parameter — it just arrives through session or header state instead of the query string, which makes it easy to miss in a code review focused only on $request->input() call sites.
What does a safe Laravel redirect pattern actually look like?
The safest pattern is to never redirect to a raw external string at all — accept only relative, same-application paths, or validate against an explicit allow-list of hosts before calling any redirect helper. A practical check combines a same-site prefix test with rejection of protocol-relative and malformed paths: if (! Str::startsWith($target, '/') || Str::startsWith($target, '//')) { abort(400); } catches both https://evil.com (fails the leading-slash check) and the classic bypass //evil.com (a protocol-relative URL that browsers treat as external despite starting with a single slash). For cases where redirecting to a different host is a legitimate feature — SSO callbacks, payment gateway returns — validate the parsed host against a config-driven allow-list rather than a blank call to away(): $host = parse_url($target, PHP_URL_HOST); if (! in_array($host, config('app.trusted_redirect_hosts'))) { abort(400); }. Neither check is exotic; the discipline is remembering to apply it every time user-influenced data reaches intended(), away(), or previous(), not just the obvious redirect($request->input(...)) call.
How Safeguard Helps
Open redirect is a design-pattern flaw more than a single tracked bug, which is exactly the kind of issue that's easy for a developer to reintroduce in a new controller months after the original review closed it out. Static analysis tuned to data flow — tracing untrusted input from a request source through to a redirect sink across functions and files — is what catches this pattern at the source-to-sink level rather than relying on a reviewer remembering the framework's footguns. Safeguard's SAST engine builds exactly this kind of source-to-sink dataflow trace and surfaces it as a finding with CWE mapping and code location as part of a normal CI pipeline; phase-one language coverage is JavaScript/TypeScript, Python, and Java, with more languages — including PHP — on the roadmap, so teams running other stacks today should pair this pattern with the manual allow-list review above.