Skip to main content

    Open Redirect

    You receive a link in your email inbox from a social media website, it’s legitimate, the domain is correct and you login successfully. But instead of being redirected to the home screen, you find yourself on a phishing website. That’s what an open redirect can look like from the victim’s perspective.

    Redirect logic is common in web applications. Login flows, payment gateways, and content sharing often rely on redirects. A successful open redirect doesn’t just fool a single user, it undermines the integrity of your domain, erodes user trust, and exposes your business to phishing campaigns. While open redirects are sometimes dismissed as “low-severity,” attackers can often chain them into something far worse. As we’ll see in this article, the impact ranges from stealing OAuth tokens for account takeover, to bypassing CSRF protections, to escalating into XSS or SSRF. In other words, what looks like a harmless detour can become the entry point to a critical breach.

    In this article, we will explore the fundamentals of open redirects, look in some more detail how attackers exploit them and escalate them. We’ll also show you what vulnerable code looks like, and finish with clear recommendations to reduce your risk.

    What Are Open Redirects?

    Redirects are a standard feature of web applications. They were designed to solve practical problems, such as sending a user back to the page they came from, directing them to a login page, or moving them to a new section of a site after an update. In short, redirects help keep the user experience smooth when content or workflow changes.

    An Open Redirect risk appears when a redirect destination is built directly from user input. For example, if a program takes a value from a query parameter and uses it as the redirect target, it effectively gives control of navigation to whoever provides that input. Features like flexible URL parsing or automatic redirect handling can increase the exposure, because they make it easier for an attacker to supply their own links.

    Exploiting Open Redirect Vulnerabilities

    One of the most straightforward attacks is redirecting users to a malicious website. Imagine an application that redirects users to a protected page. The application may redirect the users to the login page and send the original protected page as a query parameter to redirect to after logging in, such as:

    https://semgrep.dev/login?redirect_url=https://semgrep.dev/my-dashboard

    If the program redirects to whatever appears in redirect_url, an attacker can replace it with a malicious destination:

    https://semgrep.dev/login?redirect_url=https://attacker.com/my-semgrep-dashboard

    The user sees the trusted domain semgrep.dev in their browser before the redirect happens. By the time they land on the attacker’s page, it may look like a legitimate login or checkout page designed to steal credentials.

    Common Open Redirect Attacks

    To an attacker, an open redirect is often considered low-hanging fruit. However, with the increased complexity of modern applications, it’s often become possible to escalate these to higher-severity security issues. That’s why open redirects shouldn’t be dismissed as harmless. In practice, they’re often used as building blocks in larger attacks such as phishing, session fixation, Cross-Site Scripting (XSS), Server-Side Request Forgery (SSRF), or Cross-Site Request Forgery (CSRF). Here’s a few examples.

    Chaining with CSRF

    This Python example with Django demonstrates combining an open redirect with a cross-site request forgery:

    from django.http import HttpResponseRedirect, JsonResponse

    def redirect_view(request):
    url = request.GET.get("url")
    return HttpResponseRedirect(url)

    def export_semgrep_findings(request):
    if request.method == "GET":
    project_id = request.GET.get("id")
    email = request.GET.get("email")
    export_and_send_semgrep_findings(project_id, email)
    return JsonResponse({"success": True, "message": "Findings sent"})

    In this fictional example, we notice an open redirect in the redirect_view . Additionally, if CSRF protection is not enabled globally, the export_semgrep_findings function is vulnerable to a CSRF attack. The following malicious link would allow an attacker to trick a logged-in user into triggering a request that exports sensitive Semgrep findings from their project and emails them straight to the attacker.

    /redirect?url=/api/project/export?id=1234&email=pieter@attacker.com

    Chaining with SSRF

    Even if an application tries to restrict which hosts it can fetch from, an open redirect can bypass those defenses. Suppose semgrep.dev has an image loader that only allows fetching from *.semgrep.dev , the implementation might look something like the Python code snippet below.

    import requests
    from urllib.parse import urlparse
    from django.http import JsonResponse

    def image_loader(request):
    url = request.GET.get("url")
    parsed = urlparse(url)

    if not parsed.hostname.endswith("semgrep.dev"):
    return JsonResponse({"error": "Invalid host"}, status=403)
    try:
    resp = requests.get(url)
    return JsonResponse({"image_data": resp.text})
    except Exception:
    return JsonResponse({"error": "Failed to fetch"}, status=500)

    An attacker can combine this with the open redirect endpoint:

    /api/image-loader?url=http://semgrep.dev/redirect?url=http://169.254.169.254/data

    Because the requests library automatically follows redirects, the server fetches data from internal services not intended to be reachable by users, effectively turning the open redirect into an SSRF vector.

    Account takeover with OAuth

    Open redirects are especially dangerous when used in authentication flows. For example, in a simplified OAuth endpoint:

    from django.http import HttpResponseRedirect

    def oauth_start(request):
    cid = request.GET.get("client_id")
    uri = request.GET.get("redirect_uri")
    url = f"https://oauth.provider.com/auth?client_id={cid}&redirect_uri={uri}&response_type=token"
    return HttpResponseRedirect(url)

    If the redirect URI is not validated, an attacker can supply a URL they control:

    /api/oauth/start?client_id=1234&redirect_uri=https://attacker.com/callback

    When the OAuth provider completes the login flow, it sends the victim’s access token to the attacker’s server. With this token, the attacker can impersonate the victim and take over their account.

    Detecting Open Redirect Vulnerabilities in Your Code

    We’ve already seen a number of Open Redirect vulnerabilities above. To find similar issues in your code, you need to look for places where user-supplied data can control the URL of a redirect. The simplest, yet surprisingly common, data flow is when a query parameter is used to supply the redirect URL entirely.

    from django.http import HttpResponseRedirect

    def index(request):
    user_supplied_url = request.GET["redirect_url"]
    return HttpResponseRedirect(user_supplied_url)

    The program reads the value of redirect_url from the query string and redirects the user to it. But because this value comes directly from user input, an attacker can supply any domain they want. The server does not verify if the destination is trusted, which makes this a textbook open redirect.

    To detect these issues in your own code, you need to trace the flow of user-controlled input into redirect functions. This can be challenging to do manually in larger projects, because input may pass through several variables before being used. Semgrep can identify when data from request parameters or headers flows into redirect functions. It highlights risky cases while ignoring safe patterns, such as when the redirect target is built from fixed routes using mapping tables like Django’s reverse() function.

    Recommendations and Mitigations

    The most effective defense is to ensure that user input never directly controls the full redirect destination.

    If your application needs to support dynamic redirects, restrict them to a predefined list of safe domains or internal paths. Check if your library or framework provides utility functions to safely redirect to known paths, such as Django’s reverse() function.

    from django.http import HttpResponseRedirect, HttpResponseBadRequest
    from django.urls import reverse, NoReverseMatch

    def safe_redirect(request):
    target = request.GET.get("next")

    try:
    # reverse() only resolves to registered Django views
    safe_url = reverse(target)
    return HttpResponseRedirect(safe_url)
    except NoReverseMatch:
    return HttpResponseBadRequest("Invalid redirect target")

    return HttpResponseBadRequest("Redirect not allowed")

    If you must implement the allowlist yourself, be careful to implement the checks correctly. Attackers have many workarounds for common implementations. Take a look at Portswigger’s url validation bypass cheat sheet to see how creative they can get.

    Even, then, if you correctly restrict the URL, as we’ve seen in the example attacks, redirects to restricted domains can still be useful to an attacker turning them into SSRF vectors.

    If it is not possible to use any of the above defenses, another useful approach is to notify the user whenever they are leaving your site. Showing the external domain clearly gives them a chance to recognize when something looks suspicious.

    Conclusion

    Open redirects may look like a minor detail, but they can have major consequences. When untrusted input controls navigation, attackers can exploit the trust users place in your domain and redirect them to harmful destinations.

    In this article, we covered what redirects are, how attackers abuse them, how to spot vulnerable code, and what practical steps you can take to avoid them. The takeaway is simple: never let raw user input decide where your program redirects.

    Redirect logic will always be part of building modern applications, but with careful validation and the help of tools like Semgrep, you can keep your projects safe from this subtle but impactful issue.


    Not finding what you need in this doc? Ask questions in our Community Slack group, or see Support for other ways to get help.