ASP.NET MVC - Prevent Image Leeching with a Custom RouteHandler

Have you ever noticed an unusually high number of requests in your web server log files for image files? It may well be that someone is linking to your images from their own site, and basically stealing your bandwidth. Here's how to implement a custom RouteHandler within an ASP.NET MVC application to prevent people leeching your images.

To begin with, I'll start by reviewing the flow of execution when a request comes into a typical ASP.NET MVC site. When IIS receives a request, it passes the request to ASP.NET, based on the url, or more specifically, the file extension. With IIS 7 in integrated mode (the default), all requests are mapped to ASP.NET, while with IIS 6, you can set up a wildcard mapping to cause this to happen too.

MVC Routing Overview

The first component that gets invoked within an ASP.NET MVC application is UrlRoutingModule which is in fact part of System.Web.Routing. Its job is to first check the incoming url to see if there is a matching file on disk. If there is, it passes the request back to IIS to serve the file directly.  If there is no matching file on disk, the module gets to work by examining its RouteCollection structure to determine where to send the request next. It will invoke the RouteHandler associated with the matching route's entry (MvcRouteHandler by default), which in turn invokes the appropriate HttpHandler to process the remaining logic associated with the request. Again, by default, this will be MvcHandler. With image files, which do exist in a subfolder somewhere within the application, the core routing module doesn't get that far as control is passed back to IIS prior to a RouteHandler being invoked.

Usually, the fact that ASP.NET bails out of processing requests for static files is desirable behaviour. However, if you want to perform some logic prior to serving these types of requests, you need to get involved programmatically at some point.  You could get round the default behaviour for static files by setting RouteTable.Routes.RouteExistingFiles = true. Phil Haack (Senior Program Manager for ASP.NET MVC) rightly describes this as the "nuclear option" in that every file - css, js, doc, pdf, xml etc will have to be handled by Routing. So the key to this is to ensure that requests for static files do not match existing files on disk, which will force the routing module to perform its lookup against the RouteTable (and then invoke a RouteHandler etc). This is simple to do, just by ensuring that your <img> elements reference a fictitious directory. For example, if your images are located in a subfolder called "images", img tags that reference a "graphics" folder will not find existing matching files.

Having done this, the next steps are to:

  1. Register a Route for image file requests
  2. Create a RouteHandler to handle those requests
  3. Create an HttpHandler to process the actual requests

We'll start with Step 2. There's not much point in starting with Step 1 - if you try to register a route and specify a custom RouteHandler for that Route without creating one first, you will not be able to compile.

The RouteHandler is simple. It has to implement IRouteHandler, which only specifies one method - IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext):


public class ImageRouteHandler : IRouteHandler
{
  public IHttpHandler GetHttpHandler(RequestContext requestContext)
  {
    return new ImageHandler(requestContext);
  }
}

Moving quickly on to the actual HttpHandler - ImageHandler that this method returns:


public class ImageHandler : IHttpHandler
{
  public ImageHandler(RequestContext context)
  {
    ProcessRequest(context);
  }

  private static void ProcessRequest(RequestContext requestContext)
  {
    var response = requestContext.HttpContext.Response;
    var request = requestContext.HttpContext.Request;
    var server = requestContext.HttpContext.Server;
    var validRequestFile = requestContext.RouteData.Values["filename"].ToString();
    const string invalidRequestFile = "thief.gif";
    var path = server.MapPath("~/graphics/");

    response.Clear();
    response.ContentType = GetContentType(request.Url.ToString());

    if (request.ServerVariables["HTTP_REFERER"] != null &&
        request.ServerVariables["HTTP_REFERER"].Contains("mikesdotnetting.com"))
    {
      response.TransmitFile(path + validRequestFile);
    }
    else
    {
      response.TransmitFile(path + invalidRequestFile);
    }
    response.End();
  }

  private static string GetContentType(string url)
  {
    switch (Path.GetExtension(url))
    {
      case ".gif":
        return "Image/gif";
      case ".jpg":
        return "Image/jpeg";
      case ".png":
        return "Image/png";
      default:
        break;
    }
    return null;
  }

  public void ProcessRequest(HttpContext context)
  {
  }

  public bool IsReusable
  {
    get { return false; }
  }
}

At the bottom are the two methods that the IHttpHandler interface insists are implemented. The first of these, public void ProcessRequest(HttpContext context) is ignored. With an MVC application, we pass in a RequestContext object, not an HttpContext object, so an overload is provided that is MVC friendly, and it is this overload that does all the work. It is called within the ImageHandler's class constructor. In this case, ProcessRequest checks to see if my domain is part of the referer (which it should be if the image request is being made from a page in my site, and not someone else's site). If it isn't, I am returning an alternative image in the response. What you decide to return is up to you. I have seen all sorts of things returned by other people, including replacement images containing "nudies", or a simple 1 pixel transparent gif, or a plain and simple "404 Not Found".

There are other actions you could take at this point. For example, it may be that you are happy for Google to index your images, so you might want to check to see if the referer contains "images.google". You may also want to log the referer if the condition fails, indicating a potential leecher.

Now we need to register the requests for images in the RouteTable, and to indicate that ImageRouteHandler should take care of them. In global.asax, this is added:


routes.Add("ImagesRoute",
                 new Route("images/{filename}", new ImageRouteHandler()));

Hopefully, not only should this article have helped you to protect against image leeching, but should also have given you enough of the basics behind ASP.NET MVC Routing so that you can extend it as you need for other purposes.

Date Posted: Friday, December 25, 2009 10:31 PM
Last Updated: Friday, October 10, 2014 9:19 PM
Posted by: Mikesdotnetting
Total Views to date: 39301

20 Comments

Saturday, December 26, 2009 10:22 AM - Troels Thomsen

Please bear in mind that the `HTTP_REFERER` is not required and may be missing from the request. I understand that your code is only to educate and serve as an example, but if someone were to use it directly they would block a lot of legit requests from visitors having some kind of internet security application installed (many of these remove this header).

Saturday, December 26, 2009 11:10 AM - Mike

@Troels

It's a reasonable point that you make. In that case, you might decide to send the genuine image when the HTTP_REFERER is null.

Thursday, December 31, 2009 11:21 AM - CareySon

hi mike,i have a question to ask...
if every image is come through the HttpHandler instead of download directly...if the website is about picture album or something like that...
is there any performance issue we should take into account?

Thursday, December 31, 2009 12:07 PM - Mike

@CareySon,

I very much doubt it. The Handler requires less resources than a regular web page.

Thursday, February 18, 2010 11:38 AM - Mujahid Khaleel

The handler becomes more generic if the content type return mime is moved to a config file. This was you can control not only images, but pdf, doc, docx....

Mike, Thankyou for this, this article helped me quickly solve a problem, my handler also validates session for certain documents before serving.

Thursday, February 18, 2010 11:46 AM - Tommy Carlier

The HttpRequest-class has a property UrlReferrer that contains the parsed URI of the referrer. I think that might be a better option than to use ServerVariables["HTTP_REFERER"].

Thursday, February 18, 2010 1:45 PM - Denis

A bit off topic but looking at that flow diagram I wonder if it would be possible somewhere along the line on the server to decide to send back an image placeholder instead of a 404 for certain images. Doing it on the client has serveral cons.

Thursday, February 18, 2010 7:27 PM - Mike

@Tommy

Both methods point to the same thing. The only advantage UrlReferrer has is that it is strongly typed. Therefore, it may be a better option for those who are not used to dealing with Request and Response objects in the raw.

Thursday, February 18, 2010 7:29 PM - Mike

@Denis

I'm uncertain as to what you are talking about. What kind of place holder do you have in mind and what purpose should it serve?

Sunday, February 21, 2010 6:00 PM - Dubb

how does this tie into search engines? would the image be able to be retrieved by Google, Yahoo, or Bing?

Monday, February 22, 2010 9:02 AM - Denis

@Mikesdotnetting

I'm think of a scenario where you can't be sure of the quality of the data for a listing object that has images. In some cases image uri come across the wire that don't actually exist. Covering this with JS requires either a block at the top of the page or a block that doesn't execute till after page load. The former slows down the page rendering and the later mean the user can see broken images till the JS gets around to executing.

Monday, February 22, 2010 10:26 PM - Mike

@Dubb

I discuss that in the third paragraph from the end. You can add urls to the check within the 1st if statement in ProcessRequest, or you can store them in an XML file, for instance, and check against that.

Monday, February 22, 2010 10:28 PM - Mike

@Denis,

Yes, you are right. That's wildly off-topic. And still unclear to me.

Friday, February 26, 2010 12:19 AM - tadyzzz

Why not just add handler directly to web.config? Just curious ;]

Friday, February 26, 2010 7:15 PM - alefi

tadyzzz - you don't need to regiter it in web.config, so why would you?

Wednesday, June 2, 2010 5:28 PM - Mik Robbins

Tried to implement this using VWD2010 but it does recognise the ROUTES, IRouteHandler. Am I missing a reference or something? Where does the line of code for Global.asax go exactly?

Friday, June 4, 2010 7:34 AM - Mike

@Mik

The code goes in the RegisterRoutes method within global.asax.

Tuesday, September 28, 2010 6:14 PM - Rathinavelan

would like to know, how you implemented Globalization & Localization in your website, good job on this one.

thank you

Thursday, September 30, 2010 11:22 PM - Mike

@Rathinavelan

Ermmmm.. I haven't implemented that at all.

Saturday, April 2, 2011 8:33 PM - Paul

Having some trouble getting this working with mvc3 on .net 4 framework. The images are just displaying in the browser as a broken img link. I'm kind of a noob to .net so please bear with me. I've created my images folder and put my images in there. Added the above scripts in my Globals.asax.cs, the new route declaration in the registerRoutes method and created the other classes in the same file below those already in there. I am using in my cshtml file. I'm testing this in the visual studio server. I noticed that the server.MapPath says it maps to a virtual directory in its rollover help. Do I need to set up my graphics folder as a virtual directory in IIS to get this going. It looks like it's not calling the new route at all as my breakpoint in the script is not getting hit. Is this something to do with the Razor views @Url pattern? Any ideas or hints here would be appreciated.
Add your comment

If you have any comments to make about this article, please use this form to do so. Make sure that your comment relates specifically to the article above. More general comments can be posted through the form on the Contact page.

Please note, all comments are moderated, and some may not be published. The kind of things that will ensure your comment is deleted without ever seeing the light of day are as follows:

  • Not relevant to the article
  • Gratuitous links to your own site or product
  • Anything abusive or libellous
  • Spam
  • Anything in a language I don't understand including gibberish.

I do not pass email addresses on to spammers, so a valid one will assist me in responding to you personally if required.