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/phishingwhere 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 likeEvIl.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.Absoluterequirement ensures only fully-qualified URLs with schemes are validated externally - relative URLs like//evil.comorjavascript:alert()fail theTryCreate()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
destinationIdparameter 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.DestinationUrlallowing users to recognize suspicious domains (e.g.,paypa1.comwith number 1 vspaypal.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.comis 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