HIGH open redirectaspnethmac signatures

Open Redirect in Aspnet with Hmac Signatures

Open Redirect in Aspnet with Hmac Signatures — how this specific combination creates or exposes the vulnerability

An Open Redirect in an ASP.NET application that uses HMAC signatures can occur when a redirect target is derived from user-controlled data and the HMAC is computed only over a subset of that data or over data that does not include the final destination. If the application accepts a URL parameter such as returnUrl, signs a token or query string containing that parameter, and then later uses the unsigned user-provided value to perform the redirect, an attacker can supply a malicious external URL while the signature still validates. The signature may cover a token, an identifier, or a timestamp, but if the redirect logic appends or navigates to an independently supplied URL, the signature does not protect the destination.

Consider an endpoint that generates a signed link for email verification or password reset. If the endpoint builds a redirect URL by concatenating a user-supplied redirect query parameter without including it in the HMAC computation, the signature remains valid even when the parameter points to an attacker-controlled site. An example vulnerable flow:

  • Client requests: /account/confirm?email=user%40example.com&redirect=https://evil.com
  • Server signs the email (or a token derived from it) using an HMAC key, but does not include redirect in the signed payload.
  • Server responds with a redirect to the provided redirect URL after verifying only the signature on the email portion.

This pattern violates the principle that the HMAC must cover all inputs that influence security-sensitive behavior. Because the redirect destination is outside the signed scope, an attacker can craft a legitimate-looking signed request that leads the victim’s browser to a phishing site, enabling social engineering or session theft. In some designs, the application may partially mitigate risk by validating the redirect host against a whitelist, but if that check is performed after signature verification and is not enforced consistently, the Open Redirect persists.

ASP.NET-specific nuances exacerbate the issue. The framework’s flexible routing and model binding can map query string or header values directly to action parameters, making it easy to inadvertently trust unvalidated inputs. If the redirect is performed via Redirect(returnUrl) or RedirectPermanent(returnUrl) without strict validation, the framework will follow the user-supplied URL. Even when using Url.IsLocalUrl, developers must ensure it is called before the redirect and that the HMAC does not give false confidence by covering only a portion of the data chain.

Hmac Signatures-Specific Remediation in Aspnet — concrete code fixes

To remediate Open Redirect when HMAC signatures are in use, ensure the signed payload includes every parameter that affects the redirect decision. Never compute the HMAC over a token or identifier while allowing the final destination to be supplied independently. Below are two concrete approaches with code examples for ASP.NET Core.

Approach 1: Include the redirect target in the HMAC

When generating a signed token that will later trigger a redirect, incorporate the destination URL into the signed string. This binds the signature to the exact target, so any tampering with the redirect URL invalidates the signature.

// Example: Create a signed token that includes the redirect URL
using System; using System.Security.Cryptography; using System.Text;

public static class HmacHelper
{
    private static readonly byte[] Key = Convert.FromBase64String("YOUR_BASE64_KEY_HERE");

    public static string CreateToken(string email, string redirectUrl, DateTime expiry)
    {
        var payload = $"{email}|{redirectUrl}|{expiry:o}";
        using var hmac = new HMACSHA256(Key);
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
        var signature = Convert.ToBase64String(hash);
        return Convert.ToBase64String(Encoding.UTF8.GetBytes($"{payload}|{signature}"));
    }

    public static bool TryValidateToken(string token, out string email, out string redirectUrl, out DateTime expiry)
    {
        email = null; redirectUrl = null; expiry = default;
        try
        {
            var data = Encoding.UTF8.GetString(Convert.FromBase64String(token));
            var parts = data.Split('|');
            if (parts.Length != 4) return false;
            var computed = Convert.ToBase64String(new HMACSHA256(Key).ComputeHash(Encoding.UTF8.GetBytes($"{parts[0]}|{parts[1]}|{parts[2]}")));
            if (computed != parts[3]) return false;
            email = parts[0];
            redirectUrl = parts[1];
            expiry = DateTime.Parse(parts[2]);
            return DateTime.UtcNow <= expiry;
        }
        catch { return false; }
    }
}

In the controller, use the validated redirectUrl only after confirming it is local if necessary:

// Example: Controller usage
[HttpGet("confirm")]
public IActionResult Confirm(string token)
{
    if (HmacHelper.TryValidateToken(token, out var email, var redirectUrl, out var expiry))
    {
        // Optional: enforce local redirect if your use case requires it
        if (!Url.IsLocalUrl(redirectUrl))
        {
            return BadRequest("Invalid redirect target.");
        }
        return Redirect(redirectUrl);
    }
    return BadRequest("Invalid or expired token.");
}

Approach 2: Use a server-side mapping with a separate signature

Store the redirect target server-side (e.g., in a cache or database) keyed by a random identifier, and sign only that identifier. This avoids putting sensitive URLs in the client-facing token while still ensuring integrity.

// Example: Server-side mapping with signed identifier
public static string CreateRedirectToken(string stateId, DateTime expiry)
{
    using var hmac = new HMACSHA256(Key);
    var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stateId)));
    return Convert.ToBase64String(Encoding.UTF8.GetBytes($"{stateId}|{signature}"));
}

public static bool TryGetStateId(string token, out string stateId)
{
    stateId = null;
    try
    {
        var data = Encoding.UTF8.GetString(Convert.FromBase64String(token));
        var parts = data.Split('|');
        if (parts.Length != 2) return false;
        var computed = Convert.ToBase64String(new HMACSHA256(Key).ComputeHash(Encoding.UTF8.GetBytes(parts[0])));
        if (computed != parts[1]) return false;
        stateId = parts[0];
        return true;
    }
    catch { return false; }
}

With this method, the controller resolves stateId to a full redirect URL internally, ensuring the client cannot tamper with the destination. Whichever approach you choose, consistently include all redirect-influencing data in the HMAC scope and validate URLs with Url.IsLocalUrl when local navigation is required.

Frequently Asked Questions

Why does including the redirect URL in the HMAC prevent open redirects?
Including the redirect URL in the HMAC ensures that any tampering with the destination invalidates the signature. The server will reject the token because the computed HMAC no longer matches, preventing redirection to an attacker-controlled URL.
Is Url.IsLocalUrl sufficient to stop open redirects even when HMAC is used?
Url.IsLocalUrl is a useful safety net but should not replace signing the redirect target in the HMAC. HMAC binding provides integrity and non-repudiation, while Url.IsLocalUrl enforces navigation scope; using both is the most robust defense.