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:
Last Updated:
Posted by:
Total Views to date: 41992

20 Comments

- 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).

- 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.

- 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?

- Mike

@CareySon,

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

- 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.

- 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"].

- 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.

- 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.

- 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?

- Dubb

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

- 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.

- 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.

- Mike

@Denis,

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

- tadyzzz

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

- alefi

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

- 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?

- Mike

@Mik

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

- Rathinavelan

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

thank you

- Mike

@Rathinavelan

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

- 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 I end up deleting quite a lot. The kind of things that will ensure your comment is deleted without ever seeing the light of day are as follows:

  • Requests to fix your code (post a question to forums.asp.net instead, please)
  • Gratuitous links to your own site or product
  • Anything abusive or libellous
  • Spam

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

Recent Comments

Senad Mustafa 3/31/2015 8:57 AM
In response to ASP.NET MVC DropDownLists - Multiple Selection and Enum Support
Hi Mike, Thanks for the articles on dropdownlists. They are really great but I think you are one...

Black 3/28/2015 4:02 AM
In response to Displaying One-To-Many Relationships with Nested Repeaters
it's working. thank for the code...

Lorenzo 3/26/2015 8:21 AM
In response to iTextSharp - Introducing Tables
Hi Mike How can I add padding to all cells in the table? Kind Regards Lorenzo...

Satyabrata Mohapatra 3/25/2015 8:11 AM
In response to How To Send Email In ASP.NET MVC
Great article. Simple and up to the point....

Afzaal Ahmad Zeeshan 3/24/2015 8:17 PM
In response to How To Send Email In ASP.NET MVC
A great way to teach the MVC framework for sending the emails... Also, what I found helpful was the...

Jim H 3/24/2015 2:32 PM
In response to Migrating From Razor Web Pages To ASP.NET MVC 5 - Model Binding And Forms
Thank you. This helps....

wazz 3/22/2015 5:48 AM
In response to Posting Data With jQuery AJAX In ASP.NET Razor Web Pages
great info!!...

rael 3/21/2015 8:53 PM
In response to Getting the identity of the most recently added record
I spent hours trying to figure how to achieve this in C#. This article helped me. Thanks a lot...

Stephen 3/21/2015 8:48 PM
In response to Ajax with Classic ASP using jQuery
This was very helpful, thanks:)...

patrick voes 3/19/2015 10:19 AM
In response to iTextSharp - Introducing Tables
Thank you! very helpfull....