ASP.NET MVC - Prevent Image Leeching with a Custom RouteHandler
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.

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:
- Register a Route for image file requests
- Create a RouteHandler to handle those requests
- 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.
Chinese translation by CareySon: Asp.net MVC 利用自定义RouteHandler来防止图片盗链
Currently rated 4.67 by 6 people
Rate Now!
Date Posted:
25 December 2009 22:31
Last Updated:
31 December 2009 09:12
Posted by:
Mikesdotnetting
Total Views to date:
4700
Printer Friendly Version
Comments
26 December 2009 11:10 from Mikesdotnetting
@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.
31 December 2009 11:21 from 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?
31 December 2009 12:07 from Mikesdotnetting
@CareySon,
I very much doubt it. The Handler requires less resources than a regular web page.
18 February 2010 11:38 from 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.
18 February 2010 11:46 from 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"].
18 February 2010 13:45 from 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.
18 February 2010 19:27 from Mikesdotnetting
@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.
18 February 2010 19:29 from Mikesdotnetting
@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?
21 February 2010 18:00 from Dubb
how does this tie into search engines? would the image be able to be retrieved by Google, Yahoo, or Bing?
22 February 2010 09:02 from 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.
22 February 2010 22:26 from Mikesdotnetting
@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.
22 February 2010 22:28 from Mikesdotnetting
@Denis, Yes, you are right. That's wildly off-topic. And still unclear to me.
26 February 2010 00:19 from tadyzzz
Why not just add handler directly to web.config? Just curious ;]
26 February 2010 19:15 from alefi
tadyzzz - you don't need to regiter it in web.config, so why would you?


26 December 2009 10:22 from 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).