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.