4
\$\begingroup\$

In my ASP.NET MVC application there are occasions where I need to generate an absolute URI, such as when sending an email confirmation link to a user. The most straightforward approach is to use UrlHelper.Action() with the protocol parameter, and this has worked well for me.

However, now my server is behind a reverse proxy, so the absolute URIs that I'm generating are pointing to backendserver.myorg.local instead of publicsite.example.com, and that's not good for business.

So, I wrote this method to inspect the Origin header. Do you see any security problems or other reasons this might not be a good solution?

static class UrlHelperExtensions
{
    private const string OriginHeader = "Origin";

    /// <summary>
    /// Generates a full absolute URI, with HTTP(S), host name, and everything. Is reverse-proxy-aware,
    /// so will generate public URIs in the event that our server is behind a reverse proxy.
    /// </summary>
    /// <param name="urlHelper"></param>
    /// <param name="actionName"></param>
    /// <param name="controllerName"></param>
    /// <param name="routeValues"></param>
    /// <returns></returns>
    public static string AbsoluteAction(this UrlHelper urlHelper, string actionName, string controllerName, object routeValues = null)
    {
        if (urlHelper == null)
            throw new ArgumentNullException(nameof(urlHelper), $"{nameof(urlHelper)} is null.");

        var protocol = urlHelper.RequestContext.HttpContext.Request.Url.Scheme; // Should be "http" or "https"
        var localUriString = urlHelper.Action(actionName, controllerName, routeValues, protocol);

        var originValues = urlHelper.RequestContext.HttpContext.Request.Headers.GetValues(OriginHeader)
            .Where(x => !String.IsNullOrWhiteSpace(x))
            .ToList();

        if (!originValues.Any())
            return localUriString; // We are NOT behind a reverse proxy. Easy peasy, return local URI.

        var originUriString = originValues.First(); // There should only be only 1 Origin header, but theoretically it's possible to have more than 1.
        var originUri = parseOriginUri(originUriString);

        var uriBuilder = new UriBuilder(localUriString);
        uriBuilder.Scheme = originUri.Scheme; // AKA "protocol" (http or https)
        uriBuilder.Host = originUri.Host;
        uriBuilder.Port = originUri.Port;

        return uriBuilder.ToString();
    }

    private static Uri parseOriginUri(string originText)
    {
        try
        {
            return new Uri(originText);
        }
        catch (UriFormatException ex)
        {
            throw new UriFormatException($"{OriginHeader} header URI is in an invalid format: \"{originText}\"", ex);
        }
    }
}

Note: Right now I don't have any requirements to actually manipulate the URL path.

\$\endgroup\$
1
\$\begingroup\$

This is based on a misunderstanding of the Origin header. Chrome and Safari add this header to their web requests, but other browsers don't necessarily. You only have to test this from a browser like Internet Explorer or Firefox to find out it won't work.

The Forwarded header is probably more what you're looking for.

This header-based approach could be a security issue as well, depending on your environment. If your server is accessed directly somehow, then you're basically giving the remote client control over what URLs will be generated. All the client would need to do is manipulate the right headers in the right way. Giving remote clients control over your internal URLs sounds like a very bad thing.

You're probably better off using a server-side setting (perhaps in Web.config) that contains the public URI, and using that server-side setting instead.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.