Skip to content

CWE-601: Open Redirect - C# / ASP.NET

Overview

Open Redirect vulnerabilities occur when an application redirects users to URLs controlled by attackers, enabling phishing attacks and credential theft.

Primary Defence: Use Url.IsLocalUrl() (ASP.NET Core) or Request.IsUrlLocalToHost() (ASP.NET MVC 5) to validate that redirect URLs are local before redirecting. These framework methods reject external URLs, protocol-relative URLs, and JavaScript URLs, providing battle-tested protection against open redirect attacks.

Common Vulnerable Patterns

Unvalidated Redirect After Login

// VULNERABLE - Unvalidated redirect
public IActionResult Login(string returnUrl)
{
    // Authenticate user...
    return Redirect(returnUrl);  // Dangerous!
}

// Attack: returnUrl = "https://evil.com/phishing"

External URL Redirect Without Validation

// VULNERABLE - External URL redirect
public IActionResult ExternalLink(string url)
{
    return Redirect(url);  // No validation
}

Secure Patterns

Validate Local URLs Only

// SECURE - ASP.NET Core
public IActionResult Login(string returnUrl)
{
    // Authenticate user...

    if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
    {
        return Redirect(returnUrl);
    }

    return RedirectToAction("Index", "Home");
}

// SECURE - ASP.NET MVC 5
public ActionResult Login(string returnUrl)
{
    // Authenticate...

    if (!string.IsNullOrEmpty(returnUrl) && Request.IsUrlLocalToHost(returnUrl))
    {
        return Redirect(returnUrl);
    }

    return RedirectToAction("Index", "Home");
}

Why this works:

  • Framework validation: Url.IsLocalUrl() returns true only for paths starting with / (like /dashboard), rejecting absolute URLs (https://evil.com), protocol-relative (//evil.com), JavaScript URLs
  • Edge case handling: Microsoft's implementation handles double slashes (//evil.com), URL encoding (%2F%2Fevil.com), backslashes (\\evil.com), null bytes (/dashboard%00https://evil.com)
  • Fail-closed default: return RedirectToAction("Index", "Home") ensures invalid URLs redirect to safe default instead of failing open or showing errors
  • Deployment-agnostic: Works across IIS, Kestrel, reverse proxies - correctly identifies local vs external URLs regardless of hosting
  • Phishing prevention: Stops attacks like bank.com/login?returnUrl=https://evil.com/phishing where users redirected to attacker's credential-stealing site after login

Allowlist Approach

// SECURE - Allowlist of permitted domains
public class RedirectValidator
{
    private static readonly string[] AllowedDomains = new[]
    {
        "example.com",
        "www.example.com",
        "subdomain.example.com"
    };

    public static bool IsAllowedUrl(string url)
    {
        if (string.IsNullOrEmpty(url))
            return false;

        if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
            return false;

        return AllowedDomains.Contains(uri.Host.ToLower());
    }
}

public IActionResult SafeRedirect(string url)
{
    if (Url.IsLocalUrl(url) || RedirectValidator.IsAllowedUrl(url))
    {
        return Redirect(url);
    }

    return RedirectToAction("Index", "Home");
}

Why this works:

  • Explicit domain validation: Uses Uri.TryCreate() to parse the URL into components, rejecting malformed URLs that fail parsing
  • Case-insensitive host comparison: uri.Host.ToLower() prevents bypass attempts using mixed-case domains like EvIl.CoM
  • Exact host matching: AllowedDomains.Contains() performs exact host matching - rejects subdomains of attacker sites (evil.com.attacker.net), path-based bypasses (evil.com/example.com), and query parameter tricks (evil.com?domain=example.com)
  • Defense-in-depth: Combining Url.IsLocalUrl() with allowlist validation provides layered security: local URLs (like /dashboard) pass through without external validation, while absolute URLs must match the allowlist exactly
  • Comprehensive rejection: Null/empty rejection prevents edge cases where missing URLs might bypass validation; UriKind.Absolute requirement ensures only fully-qualified URLs with schemes are validated externally - relative URLs like //evil.com or javascript:alert() fail the TryCreate() check and are rejected
  • Ideal for trusted partners: This approach is ideal for trusted partner integrations where you need to redirect to specific external domains (e.g., payment processors, OAuth providers) while blocking all other external redirects

Indirect Redirects (Best Practice)

// SECURE - Use identifiers instead of URLs
public IActionResult Redirect(int destinationId)
{
    var allowedDestinations = new Dictionary<int, string>
    {
        { 1, "/dashboard" },
        { 2, "/profile" },
        { 3, "/settings" }
    };

    if (allowedDestinations.TryGetValue(destinationId, out string url))
    {
        return Redirect(url);
    }

    return RedirectToAction("Index", "Home");
}

Why this works:

  • Eliminates URL injection entirely: Indirect redirects never accept user-controlled URL strings - users provide integer IDs instead, which cannot contain malicious URLs
  • Safe dictionary lookup: TryGetValue() maps safe integer keys (1, 2, 3) to pre-defined destination URLs controlled by the application, not the user
  • Immune to manipulation: Attackers cannot inject arbitrary URLs because the dictionary keys are integers - even if they manipulate the destinationId parameter to values like -1, 999, or <script>alert('xss')</script>, the dictionary simply won't contain those keys and the lookup fails safely
  • Fail-closed default: Returns users to the homepage when an invalid ID is provided, preventing information disclosure about valid redirect targets
  • Simplified security: Complete decoupling of user input from redirect destination means code review and security audits focus on validating the hardcoded URL list rather than complex URL parsing logic
  • Most secure pattern: OWASP recommends indirect references specifically because they're immune to injection attacks; ideal for post-login redirects, workflow navigation, and any scenario where redirect targets are known at development time

Warning Page for External URLs

// SECURE - Show warning before external redirect
public IActionResult ExternalRedirect(string url)
{
    if (Url.IsLocalUrl(url))
    {
        return Redirect(url);
    }

    // External URL - show warning page
    return View("ExternalRedirectWarning", new { DestinationUrl = url });
}

// ExternalRedirectWarning.cshtml
@model dynamic
<h2>You are leaving our site</h2>
<p>You are about to visit: @Model.DestinationUrl</p>
<a href="@Model.DestinationUrl">Continue to external site</a>
<a href="/">Stay here</a>

Why this works:

  • Breaks phishing flow: Interstitial warning interrupts seamless auto-redirect to https://evil.com/fake-login, forcing explicit user awareness
  • URL inspection: Displays full @Model.DestinationUrl allowing users to recognize suspicious domains (e.g., paypa1.com with number 1 vs paypal.com)
  • Explicit user action: Users must click "Continue" link rather than passive redirect, giving time to evaluate trustworthiness
  • Safe escape: "Stay here" option provides clear alternative if redirect seems suspicious
  • Combined with local check: Url.IsLocalUrl() skips warning for local URLs (/dashboard), avoiding friction for legitimate intra-site navigation

Input Validation

public class UrlValidator
{
    public static bool IsLocalUrl(string url)
    {
        if (string.IsNullOrEmpty(url))
            return false;

        // Reject absolute URLs unless they're to our domain
        if (url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
            url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ||
            url.StartsWith("//", StringComparison.Ordinal))
        {
            return false;
        }

        // Must start with / for relative URLs
        return url.StartsWith("/") && !url.StartsWith("//");
    }
}

Verification

To verify redirect protection is working:

  • Test local URLs: Verify relative URLs (e.g., /dashboard, /profile/edit) are allowed
  • Test external URLs: Confirm absolute URLs (e.g., https://evil.com) are rejected or require allowlist approval
  • Test protocol-relative URLs: Ensure //evil.com is blocked
  • Test JavaScript URLs: Verify javascript:alert('xss') is rejected
  • Test edge cases: Try encoded URLs, mixed case, null bytes, and Unicode variations
  • Review code: Search for all uses of Redirect(), RedirectToAction(), and URL parameters
  • Check allowlists: If external redirects are permitted, verify only trusted domains are in the allowlist
  • Test after authentication: Attempt open redirect attacks in login flows and other authentication scenarios

Additional Resources