Recently at work I needed to allow for the download of sensitive files over the web. Right away your mind should jump to the security issues involved with such an undertaking. How do you allow for download without compromising the sensitive data? There is a simple but flexible method that will work perfectly.

Source code for this project.

I found the method without any difficulty using Google. The site outlined how to create a secure file download page. While implementing this solution I ran into some issues so I figured I would present my improved version and expand on some helpful features of ASP.Net.

The first step is to create a page that will list files available to download. Each link simply gives different parameters to the download script specifying which file was selected.

<form id="form1" runat="server">
  <p>Select a file to download.</p>
  <div>
      <ul>
      <li><a href="download.aspx?file=<%=Server.URLEncode("D:\\file1.txt") %>">file 1</a></li>
      <li><a href="download.aspx?file=<%=Server.URLEncode("D:\\file2.txt") %>">file 2</a></li>
      <li><a href="download.aspx?file=<%=Server.URLEncode("D:\\file3.txt") %>">file 3</a></li>
      <li><a href="download.aspx?file=<%=Server.URLEncode("D:\\test\\file4.txt") %>">file 4</a></li>
      </ul>
  </div>
</form>

The Server.URLEncode function is necessary here to make sure the filename doesn’t get mangled. It replaces any special URL characters (such as : and \) with a % code. For example you have probably seen %20 in a few URLs before, it is a URL encoded space character. Now we will need to receive the file and verify the user is authenticated.

' Make sure the user should be able to download using
' your authentication scheme then send file
Sub Page_Load()
    ' Forms authentication check
    If Not User.Identity.IsAuthenticated Then
        ' redirect to login page
    End If
    ' URL key passing
    If CheckKey(Request.QueryString("key")) Then
        ' redirect to invalid key page
    End If
    ' Session key
    If CheckKey(Session("key")) Then
        ' redirect to invalid key page
    End If

    ' Make sure a file has been specified
    Dim filename As String = Request.QueryString("file")
    If String.IsNullOrEmpty(filename) Then
        ' redirect to no file specified page
    End If

    ' Make sure file exists
    If Not File.Exists(filename) Then
        ' redirect to file does not exist page
    End If

    ' Write file to browser
    Response.ContentType = "application/octet-stream"
    Response.AddHeader("Content-Disposition", "attachment; filename=""" & Path.GetFileName(filename) & """")
    Try
        Response.TransmitFile(filename)
    Catch ex As Exception
        ' Redirect to error opening file
    End Try
End Sub

Function CheckKey(ByVal key As String) As Boolean
    ' Validate user key
    Return True
End Function

I will not cover authentication here but the first step is to check to make sure the request to the download page is by a valid user. I have provided three skeletal checks in the code but you would use the one which matches your authentication scheme. After verifying the user is valid you may wish to check if that valid user is allowed to download this file based on some internal logic. (For example upload/download ratio sites, various user level sites, etc.) Once the user has been validated it is time to validate the request. The first check is to make sure a filename was passed. We then try to open that file and read in its contents. If at any point there is a problem we simply redirect the user to an error page.

Security Note: This method exposes the internal structure of your servers file system by passing it through the URL to the download page. It is not recommended to use this method AS IS in a production environment. A clever hacker could easily change the URL passed to the download script to point to a file in any directory on your server. Normally this isn’t an issue since the built in windows permissions should deny IIS/ASP.Net from accessing anything sensitive, however, I have seen incorrectly configured servers before.

' Write file to browser
Response.ContentType = "application/octet-stream"
Response.AddHeader("Content-Disposition", "attachment; filename=""" & Path.GetFileName(filename) & """")
Try
    Response.TransmitFile(filename)
Catch ex As Exception
    ' Redirect to error opening file
End Try

The final piece of code is the most important. The response object is the output we are going to send the users web browser. At this time we can change anything we like so as to masquerade our ASP.Net page as something else. For example you can set the response up as an image, use an image library and create dynamic thumbnails of images. Then your IMG src would be the .aspx page. In this case we want to send the user a file so we change the response to an octet-stream (binary data) and give the name of the attached file. The major bug that I fixed is to add the quotes to the response around the filename. This is very important since other wise browsers don’t get the full filename if it contains spaces. Additionally, some browsers have issues with this attachment method without the quotes.

Path.GetFileName is a very handy tool to grab the filename off the end of a path string. There are a whole set of static methods in the Path object that you can use in this fashion to take parts of the path or url that you wish without having to parse them yourself. Make sure to browse throught the MSDN page for the Path class (scroll down to the Public Members section).

One potential caveat is the downloading of very large files as described in this MSDN knowledge base article. However, a method is presented which should work. Another possible issue is with Response.TransmitFile itself, see this Microsoft fix.

If you are interested in some specific topic related to ASP.Net let me know and I’ll cover it in a future post. I am always interested to hear what types of things people are doing with .Net.