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

28 Comments

- bincoder

Mike, your site is great, thx again

- Abdul Rauf

Nice article. I was looking for that.

- ALi

goodie

fair and clean demonstration, i like it

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

- rtpHarry

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

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

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

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

- liyou

good article what i need.

- 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

- 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

- Catalin D.

Very nice your article.

- Cam Luc

This is very helpful.

- Jay

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

- abex

it is important for undergraduate students.10Q

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

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

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

- darcy

how do you add the filles to App_Data

- darcy

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

- Yogendra

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

- kisan

is it possible to protect video from download

- Mike

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

- gfox

thank you so much

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

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

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

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

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

Bigmachini 3/19/2015 6:13 AM
In response to ASP.NET MVC DropDownLists - Multiple Selection and Enum Support
This just made my day, afternoon, evening, night... was looking for a way of doing this without to a...

Bobbyg 3/19/2015 4:14 AM
In response to HTML Helpers For Forms In Razor Web Pages
Nice article. There are bugs in MVC 5 with dropdowns retaining values from other screens and them. I...