18

I have an ASP.NET 6 MVC application running in Azure. I have a controller with an action like

[HttpDelete]
[Route("{image_url}")]
public async Task<IActionResult> RemoveImageUrl([FromRoute(Name = "image_url")] String imageUrlString)

I then call it like

api/https%3A%2F%2Frepocreator.zoltu.io%2Fimages%2FZoltu-Logo-Full-Size.png"

This application works fine when self hosting with Kestrel, but as soon as I deploy to Azure I get 500 errors. I have debugged as much as I can and after a lot of Googling and poking it appears that IIS is attempting to helpfully URL Decode the request before forwarding it on to ASP.NET to handle. The problem, of course, is that even if I can convince IIS to accept the request with

<system.webServer>
    <security>
        <requestFiltering allowDoubleEscaping="true" />
    </security>
</system.webServer>
<system.web>
    <httpRuntime requestValidationMode="2.0" requestPathInvalidCharacters="" relaxedUrlToFileSystemMapping="true"/>
    <pages validateRequest="false" />
</system.web>

It still decodes the URL and passes the decoded URL on to ASP.NET which doesn't find a matching route.

What can I do to tell IIS to stop trying to be helpful here and just pass along whatever URL it gets, without doing any sort of pre-validation or rewriting along the way? Note: this is an Azure Web App, so I don't have direct access to IIS settings.

Daniel J.G.
  • 34,266
  • 9
  • 112
  • 112
Micah Zoltu
  • 6,764
  • 5
  • 44
  • 72

1 Answers1

5

You could just update your route definition so it matches a decoded image url parameter.

As per the documentation, when defining route templates:

You can use the * character as a prefix to a route value name to bind to the rest of the URI. For example, blog/{*slug} would match any URI that started with /blog/ and had any value following it (which would be assigned to the slug route value).

So you could create an action matching the route [Route("{*image_url}")]:

[Route("{*image_url}")]
public IActionResult RemoveImageUrl([FromRoute(Name = "image_url")]String imageUrlString)
{
    return Json(new { ReceivedImage = imageUrlString });
}

The only problem I have seen is that the protocol part is decoded as http:/ with a single /. You have a couple of options:

  • You could manually fix it in the controller. Even better, you could create a model binder and a parameter convention to do that automatically:

    [HttpDelete]
    [Route("{*image_url}")]
    public IActionResult RemoveImageUrl([FullUrlFromEncodedRouteParam(Name = "image_url")] String imageUrlString)            
    {
        return Json(new { ReceivedImage = imageUrlString });
    }
    
    public class FullUrlFromUrlEncodedPathSegmentModelBinder : IModelBinder
    {
        //Matches a url that starts with protocol string and is followed by exactly ":/" instead of "://"
        private static Regex incorrectProtocolRegex = new Regex(@"^([a-z][\w-]+:)\/{1}(?!\/)");
    
        //A url path included as a url encoded path segment like http://localhost:39216/image2/https%3A%2F%2Frepocreator.zoltu.io%2Fimages%2FZoltu-Logo-Web.png
        //will be decoded by IIS as https:/repocreator.zoltu.io/images/Zoltu-Logo-Web.png, note the single '/' after the protocol
        //This model binder will fix it replacing "http:/" with "http://"
        public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext.ValueProvider.GetValue(bindingContext.ModelName) == null)
                return Task.FromResult(ModelBindingResult.NoResult);                            
    
            var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).FirstValue as string;
            var fixedVal = incorrectProtocolRegex.Replace(val, @"$1//");                                                
            return Task.FromResult(ModelBindingResult.Success(bindingContext.ModelName, fixedVal));                        
        }
    }
    
    public class FullUrlFromEncodedRouteParamAttribute : Attribute, IParameterModelConvention
    {
        public string Name { get; set; }
    
        public void Apply(ParameterModel parameter)
        {
            parameter.BindingInfo = parameter.BindingInfo ?? new BindingInfo();
            parameter.BindingInfo.BinderModelName = Name;
            parameter.BindingInfo.BindingSource = BindingSource.Path;
            parameter.BindingInfo.BinderType = typeof(FullUrlFromUrlEncodedPathSegmentModelBinder);         
        }
    }
    
  • A better approach might be updating your api so you don't even use the protocol part in the image key. That will let you add the proper protocol to the full image url when you need to render it, depending on whether it needs http or https (Even the host could be omitted from the urls). You wouldn't even need to worry about url encoding the image path on your client side, you could just invoke it like http://localhost:39216/api/repocreator.zoltu.io/images/Zoltu-Logo-Full-Size.png.

IMHO I would prefer the second approach. If you really need the full url encoded in the route, then at least you have a way of implementing it in a clean way outside the controller.

Note: If you want to keep the protocol part in the image url, it looks like the static files middleware does not like them so it has to be added after MVC in Startup.configure, otherwise it will throw errors.

Daniel J.G.
  • 34,266
  • 9
  • 112
  • 112
  • 3
    While your suggestion would allow me to work-around the immediate issue (with changes on both the front-end and backend), it doesn't address the root question which is how to make IIS in Azure *NOT* URL decode request before passing it along to my framework. The IIS behavior is against the URL spec and I really want a way to use URL Encoding the way it was designed, and also in a way that is portable across web servers and flexible (API users don't have to know about the oddity of IIS). – Micah Zoltu May 22 '16 at 04:02
  • I agree it doesn´t change how Azure treats urls, but users won´t need to know about Azure specifics isnt? Both `http://localhost:39216/api/repocreator.zoltu.io%2Fimages%2FZoltu-Logo-Full-Size.png` and `http://localhost:39216/api/repocreator.zoltu.io/images/Zoltu-Logo-Full-Size.png` would work correctly. – Daniel J.G. May 22 '16 at 19:24
  • Unless I am misunderstood what you have proposed, if a user of my API calls it with `.../api/https%3A%2F%2Frepocreator.zoltu.io%2Fimages%2FZoltu-Logo-Web.png` things will fall apart because IIS will misinterpret the URL Encoded `//`. This means that a user behaving reasonably will fail because the backend happens to be running on IIS (and not handling URL Encoding correctly). If the backend were running on Kestrel the request would have been valid. – Micah Zoltu May 23 '16 at 00:01
  • Yes, if you really want to include the protocol in the api then you have that problem (which you could still fix in the controller). However if users don't need to url encode the full path and don't even need to worry about the protocol or host is a good thing IMHO (as in the [dropbox api v1](https://www.dropbox.com/developers-v1/core/docs#files-GET) or [api v2](https://www.dropbox.com/developers/documentation/http/documentation#files-download)). What happens to those images if you change your host or use https? – Daniel J.G. May 23 '16 at 08:35