Simple File Download Protection with ASP.NET

When it comes to protecting files from unauthorised downloading, the vast majority of articles offer solutions that involve mapping common file types (.pdf, .txt, .doc etc) to ASP.NET within Internet Information Services.  However, often in a shared hosting environment, you don't have access to IIS and the hosting company will not agree to providing such mappings for you, so what do you do?

Let's start by examining the problem is a little more detail first.  Forms Authentication in ASP.NET only works with requests that are handled by ASP.NET in IIS 6.0 (typically Windows Server 2003), which means that non-ASP.NET content bypasses aspnet.dll, and is not subject to it. Let's say you have a folder within your application called Private.  You set up Forms Authentication to protect this folder, such as in the following web.config snippet:


<system.web>
  <authentication mode="Forms">
    <forms loginUrl="Private/Login.aspx" defaultUrl="Private/Default.aspx">

      <credentials passwordFormat="Clear">
        <user name="mike" password="test" />

      </credentials>
    </forms>
  </authentication>
</system.web>
<location path="Private">

  <system.web>
    <authorization>
      <deny users="?" />
    </authorization>

  </system.web>
</location>

The URL to your Private folder is http://www.mysite.com/Private.  Any requests for that URL will invoke the default document, which in this case is Private/Default.aspx.  Since that has been protected under Forms Authentication, and since all .aspx files are mapped to aspnet.dll, Forms Authentication kicks in and users who have not already logged in will be redirected to Private/Login.aspx.  Login.aspx contains a straightforward Login Control:


<form id="form1" runat="server">
<div>

  <asp:Login ID="Login1" runat="server" onauthenticate="Login1_Authenticate" />

</div>
</form>

And the code-behind contains the authentication logic:


protected void Login1_Authenticate(object sender, AuthenticateEventArgs e)
{
  string username = Login1.UserName;
  string password = Login1.Password;
  if(FormsAuthentication.Authenticate(username, password))
  {
    FormsAuthentication.RedirectFromLoginPage(username, false);
  }
}


Users who successfully authenticate will be directed to Default.aspx, which contains links to downloadable files:


<form id="form1" runat="server">
<div>
<a href="HelloWorld.txt">Click Here to Get File</a>

</div>
</form>

This seems to work, but if a non-authenticated user just enters http://www.mysite.com/Private/HelloWorld.txt into their browser, the file will be served, as ASP.NET is not configured to handle .txt files.  As I mentioned before, the vast majority of articles on this topic show how to map .txt to aspnet.dll within IIS, create an HttpHandler to manage the file access, and then register that handler within the web.config file.  If you do not have access to IIS, there is a simple workaround.  The first thing to do is to move all download files to a location where they cannot be browsed.  Ideally, your web hosting company will have provided you with access to at least one folder above the root folder of your application.  This is ideal, because no one can browse that folder since it is not part of the application itself.  However, if you only have access to the root folder and its contents, there is still at least one other option - App_Data.  Anything placed in App_Data is protected by ASP.NET, and requests for items within it are met with a 403 - Forbidden error message.

Once you have moved your files, you need a means to serve them to authenticated users, and an HttpHandler will do the job easily.  Just go to Add... New Item, and select Generic Handler.  You should be met with a new file with a .ashx extension containing code like this:


public class MyFileHandler : IHttpHandler
{

  public void ProcessRequest(HttpContext context)
  {
    context.Response.ContentType = "text/plain";
    context.Response.Write("Hello World");
  }

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


The Handler contains two methods - ProcessRequest and IsReusable.  The first houses the logic that needs to be run to process the current request, and the second dictates whether the handler can be pooled and reused for other requests.  For the sake of simplicity, the default value of false can be left as it is.  The point about the handler, created from the Generic Handler option with its .ashx extension is that it is already mapped to aspnet.dll, so it can take part in Forms Authentication.  Not only that, but it does not need to be registered within the web.config file. Now its simply a matter of adding some logic to validate the user, and retrieve the file they are after:


public class MyFileHandler : IHttpHandler

{

  public void ProcessRequest(HttpContext context)
  {
    if (context.User.Identity.IsAuthenticated)
    {
      string filename = context.Request.QueryString["File"];
      //Validate the file name and make sure it is one that the user may access
      context.Response.Buffer = true;
      context.Response.Clear();
      context.Response.AddHeader("content-disposition", "attachment; filename=" + filename);
      context.Response.ContentType = "application/octet-stream";

      context.Response.WriteFile("~/App_Data/" + filename);
    }
  }

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

This is really simple.  After establishing whether the current user is authenticated, the handler checks the querystring for a filename. At that point it is important to validate the filename, to make sure that it is one that the user can have access to. It is possible for a user to alter the querystring to point to folders above App_Data, such as the root directory, and request web.config by passing ../web.config into the querystring.  This will throw an exception on most servers, as the ../ notation for parent paths is disabled by default.  However, as a couple of commentators have mentioned below, this is not always the case.

Then the method simply uses Response.WriteFile() to deliver the file.  I have set the ContentType of the file to application/octet-stream, and the content-disposition to attachment above, which will cover any type of file and always force a Save or Open dialogue box.  You may prefer to check the file extension and set the ContentType accordingly.  You may also want to add some error checking logic to ensure that a querystring value has been passed, that the file exists etc.

However, if you are not using FormsAuthentication out of the box, you may be checking a session variable on each page to see if the user is logged in instead.  This being the case, you need to know that HttpHandlers do not have access to session state by default, so references to session variables will fail.  One change is all that is required, and that is to make your HttpHandler implement IReadOnlySessionState (or IRequiresSessionState if you want to modify session variables).  The HttpContext object that is passed in to the ProcessRequest() method provides access to session variables through its Session property.


public class MyFileHandler : IHttpHandler, IReadOnlySessionState

{
 ....
 

 

That just leaves one question - how does the file name get into the querystring?  Going back to Private/Default.aspx, we simply amend the link to point to the handler instead:


<form id="form1" runat="server">
<div>

<a href="MyFileHandler.ashx?File=HelloWorld.txt">Click Here to Get File</a>
</div>
</form>

There we have it Simple authentication checks made before delivering files, without having to register the handler in the web.config file, or mess about with IIS settings.  If you are being hosted on a server that runs IIS 7.0, things are a lot easier. With its new Integrated Pipeline model, a simple change to your application's web.config file will ensure that all content within your application is always handled by ASP.NET, so that non-ASP.NET content can take part in ASP.NET Forms Authentication.  This article details how to make that change.

 

Date Posted: Wednesday, November 25, 2009 9:09 AM
Last Updated: Saturday, July 19, 2014 6:52 PM
Posted by: Mikesdotnetting
Total Views to date: 90281

28 Comments

Thursday, November 26, 2009 5:35 PM - bincoder

Mike, your site is great, thx again

Friday, December 4, 2009 5:09 AM - Abdul Rauf

Nice article. I was looking for that.

Friday, December 4, 2009 6:22 AM - ALi

goodie

fair and clean demonstration, i like it

Friday, December 4, 2009 8:31 AM - CyberFive

One problem with this solution. A logged in user suddenly has access to a lot of other files too. What is preventing the user from changing the href to "MyFileHandler.ashx?File=../web.config"
and getting the web.config send?
I know a lot of hosters prevent the use of .., but not all.

Friday, December 4, 2009 8:44 AM - rtpHarry

Mike, what would happen if I called for example MyFileHandler.ashx?File=../web.config

Friday, December 4, 2009 9:27 AM - Mike

@ CyberFive and rtpHarry

Both of you have made a very valid point. My article was intended simply to illustrate how to achieve the objective, and as with many articles of this type, I have not cluttered it with exception handling or validation. I'm always in two minds whether including these obscures the general points, given that I believe most of the people reading my site are at beginner level.

In this specific case, a simple validation check on the query string value, and a whitelist of file extensions will help prevent people downloading config files etc where parent paths have not been disabled. It will amend the article to highlight the need to validate the querystring.

It sort of goes without saying that you should never ever trust user input, and mentally, since I have made that point in many of my articles, I guess that I fall into the trap of thinking readers have been told. I keep forgetting that each article might be the first one a reader sees.

Friday, December 4, 2009 4:26 PM - rtpHarry

When I am writing my articles I am usually conscious that people will likely just copy and paste my code straight into their projects so I try to include the validation and error checking as it is essential for the code to be usable.

If you are trying to allow access to files that users have uploaded then storing the filenames in a database would work and then you can use the file id in the querystring which isn't susceptible to this kind of attack.

Friday, December 4, 2009 4:41 PM - Mike

@rtpHarry

I am also conscious that people may just be looking for a quick fix, in which case my articles are not geared for that. They are intended to be educational, and the breaks in code and absence of downloads hopefully discourages the copy/paste coder from lingering on my site too long. I keep getting asked to provide downloads, but consistently refuse to.

Sunday, December 6, 2009 12:00 PM - liyou

good article what i need.

Tuesday, December 8, 2009 8:09 AM - A-Dubb

In order to bypass ../web.config, you could use the static System.IO.Path class to retrieve the file name portion of the path and ignore the rest. After that you could simply append the value to "~/App_Data" like so:

string fileToServe = string.Format("~/App_Data/{0}", Path.GetFileName(path));

You can even check to ensure that the provided filename has an extension like so:

if(Path.HasExtension(filename))
// execute code here

Monday, February 22, 2010 9:58 AM - Taliesin

There a number of other issues related to compression, client & server side caching and partial downloads that should be taken care of when you serve content from http handlers. Otherwise visitors are going to be hitting your handler very hard and using up all your bandwidth.

For one that takes care of this features, check out:
http://code.google.com/p/talifun-web/wiki/StaticFileHandler

Saturday, February 27, 2010 9:22 AM - Catalin D.

Very nice your article.

Saturday, March 27, 2010 4:52 PM - Cam Luc

This is very helpful.

Thursday, June 3, 2010 10:50 PM - Jay

Using HttpResponse.TransmitFile() is propably the more appropiate way than using Response.WriteFile().

Saturday, June 5, 2010 7:19 AM - abex

it is important for undergraduate students.10Q

Thursday, December 23, 2010 12:27 PM - abhimanyu

very-very nice article. is there any way to restrict the files which is only zipped and exist in a folder named downloads inside wwwroot directory of shared hosting server. by default download directory will be accessible but i am wishing to protect this directory for all jobs. only authenticated user can access this.

Thursday, December 23, 2010 5:12 PM - Mike

@abhimanyu,

To prevent visitors browsing files, you need to put them in a folder which doesn't allow access, such as one outside of the root or App_Data.

That's the point of the article.

Saturday, January 15, 2011 4:18 PM - Jonathan Wood

Note that with ASP.NET 4.0 and IIS7, it's now a simple matter to map a particular file type to a custom handler. And it can be done strictly through web.config without delving into IIS. I just covered this topic in an article at http://www.blackbeltcoder.com/Articles/asp/writing-a-custom-http-handler-in-asp-net.

Wednesday, October 10, 2012 7:21 AM - darcy

how do you add the filles to App_Data

Wednesday, October 10, 2012 10:50 PM - darcy

when i click on the link from the homepage it just takes me to a white page and does nothing

Tuesday, March 26, 2013 12:09 PM - Yogendra

this is that article I was searching a lot thanks mike for your experiance sharing.

Wednesday, August 7, 2013 3:44 AM - kisan

is it possible to protect video from download

Thursday, August 8, 2013 7:43 AM - Mike

See either my reply to abhimanyu or the link that Jonathon Wood provided.

Sunday, November 10, 2013 2:53 PM - gfox

thank you so much

Thursday, November 21, 2013 4:42 PM - Bob

Instead of using the file name, use a GUID as the filename. Then check the directory for the guid filename. If it exists, send it, if not throw error or display message accordingly.

Thursday, December 12, 2013 1:23 AM - Michael Hidalgo

You are not properly Sanitizing the request parameter
context.Request.QueryString["File"];. A bad guy could enter any arbitrary value in the File parameter and compromise the system.

Thursday, December 12, 2013 5:50 AM - mikesdotnetting

@Michael,

I leave it to the user to validate the input. There is a comment in the code to that effect and the article also points out the importance of validating the file name and the potential dangers of not doing so.

Saturday, June 14, 2014 7:30 AM - Kelsey piguet

I don't understand
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.

Recent Comments

Manas 11/27/2014 5:30 AM
In response to Scheduled Tasks In ASP.NET With Quartz.Net
Hi Mike, Thank you for awesome article. My concern is it might impact website if we use or is...

priya 11/26/2014 6:50 PM
In response to Create PDFs in ASP.NET - getting started with iTextSharp
very nice.....its save my time...

ransems 11/24/2014 12:29 AM
In response to Adding A Controller
Love the article. I dislike that the world thinks c# articles are the way to go. Love the VB, keep...

Gautam 11/20/2014 8:01 AM
In response to I'm Writing A Book On WebMatrix
Hello Mike, I read your book, loved it! However, I have a few request/suggestions: 1) an example...

Bret Dev 11/19/2014 8:39 PM
In response to The Difference Between @Helpers and @Functions In WebMatrix
Excellent post! One concern - where can you place global @Functions code within an MVC project to Is...

Rob Farquharson 11/19/2014 4:28 PM
In response to iTextSharp - Links and Bookmarks
How can I place text at an absolute position on the page? Also, how can I rotate text?...

Andy 11/17/2014 8:08 PM
In response to MVC 5 with EF 6 in Visual Basic - Sorting, Filtering and Paging
Hello I'm testing your sorting instructions above. This is great and I was able to get it to work...

Gautam 11/17/2014 5:51 PM
In response to WebMatrix - Database Helpers for IN Clauses
Hi Mike, I am very new to programming: In the above example if I want to use a delete button the...

donramon 11/17/2014 3:22 PM
In response to Entity Framework 6 Recipe - Alphabetical Paging In ASP.NET MVC
Congratulations on your new website look and the excellent articles. Thank you!...

Gautam 11/17/2014 11:26 AM
In response to Looking At The WebMatrix WebGrid
Hi Mike, I add the jquery script at the end of my html file.. when ajax attribute is added to the be...