Secure File Download Page






4.24/5 (34 votes)
Apr 20, 2005
5 min read

360559

6323
How to design a page that protects your downloadable files.
Introduction
If you've developed any kind of e-commerce system that offers downloadable files, you've faced this problem. How do I protect my files from theft? Very often, the problem doesn't even concern payment. Many web-sites offer useful files for free. All they ask in return is that the person downloading the file simply enters some personal details, or fills out a form for the administrator's records. All too often, people don't even want to do that and they seek ways to circumvent the web server's security.
The key problem in this scenario is the fact that, no matter which way you look at it, ultimately, you have to offer users a link to the downloadable file. Once someone has that link, they can offer it to their friends and colleagues, who then simply circumvent all of your security features and download the file directly. None of the security features you put in place are worth anything if a user knows that final link to the downloadable file itself. I've seen sites that go to great lengths to "hide" these links (including masking the link by writing specific text in the browser's status bar when a user hovers over it). A simple "View Source" circumvents all these security measures.
The problem lies in the fact that your downloadable file (whether it be a .pdf, .doc, or .zip - or even a .exe) has no way to decide whether it should serve itself or not. You can't place logic in these files to decide whether or not to serve them.
One alternative is to place system security on the folder in which these files reside. This is not really viable though, as you would need to create a new system (Windows) account for each registration, send the person their username & password, and then remove their user account from the system once they have downloaded the file. This makes for an administration nightmare (not to mention an even bigger system security issue). And if you're hosting your site with an external ISP, it's highly unlikely that they'll allow you to create and delete user accounts on their server at will.
The simple solution below closes the above security hole by storing the downloadable file outside a web-site and then serving the file directly from a web page. In other words, you might link to MyFileServer.aspx and receive MyPdfDownload.pdf, for example - with no actual link to MyPdfDownload.pdf offered anywhere on your web-site.
Using the code
The code is fairly straightforward. What we're going to do is open a file stream and serve that instead of the usual text content. In other words, our .aspx page is going to behave more like a file-download. I'm using VB.NET, but this can just as easily be done using C#.
My code below assumes that you've created a folder called C:\My External\File Path\. Note, this folder exists in the C:\ drive and doesn't appear anywhere near C:\Inetpub\wwwroot or any other folder that serves as a web-site on the server. I'm assuming a file called MyPdfDownload.pdf exists in the above folder. You can add any file/ path you choose. Simply update the variables in the appropriate line of code below.
To design the application, first create a folder called C:\My External\File Path\. Then find a .pdf file (you can select any downloadable (.zip, .exe, .doc, .xls etc.) and store it in the above directory. Name it MyPdfDownload.pdf, or change the code below to reflect the file name you've saved in the above folder..
Now check that the directory has appropriate access for the ASP.NET account:
- navigate to c:\My External\.
- right-click on the File Path folder and click on Properties.
- Select the Security tab.
- If you can't see the ASP.NET account, add it.
- Highlight the account and give the user Full Control.
- Click Apply or OK.
Good. Now you have a folder outside the web. Now create a new ASP.NET web application in Visual Studio .NET.
Add a new Web Form and call it MyFileServer.aspx. Double-click on the page. This will open the code-behind (MyFileServer.aspx.vb) file and create your Page_Load
event handler.
We're going to place our code inside this Page_Load()
subroutine. This means that every time users link to our page, it will validate them and decide whether to download our external page or not. Think of this as a delegate function, or an alias for the actual file we want to download. The only difference is that now we can put server-side logic in place to decide whether or not we are prepared to serve the file. In other words, we're wrapping some intelligence around our downloadable file that verifies whether or not it should download itself.
Let's start coding.
' First, we need to declare our variables
' We'll need a filestream object to fetch the pdf file from its location
' outside the web-site.
Dim fs As FileStream
' As we're not serving standard html,
' we'll need to tell the browser what type of content we're serving
' This value will need to be added to our response header.
Dim strContentType As String
' This is the path to the folder where our file will be kept.
' Note, we DON'T use the
' Server.Mappath() method, as this would defeat the object.
' We want to keep the file outside the web application, so
' we can't use standard server object methods.
' In a real-world application you would store this value in a database
' or otherwise in web.config so that you can change it easily
' without having to recompile your code. I've hard-coded it here
' for readability. Note, you will have to ensure that the ASP.NET
' user account on your system has appropriate access rights to this folder
Dim strPath = "C:\My External\File Path\"
' We need to use this file name
' in the headers that we send to the browser. Obviously
' we don't want to show the user the full path
' to our file on the server,
' so I'm not including this in the path variable above.
' Even though they won't be able to access
' it without admin rights on your server, it will only confuse them.
' Users only need to see the file name,
' which they can then save on their own system once downloaded
Dim strFileName As String = "MyPdfDownload.pdf"
' Next we call our validation function to determine
' whether or not we are prepared to offer the user this file
' I'm not including that function in this article.
' You can create your own validation
' function based on your site's requirements
' You can even include code in this page to indicate
' whether a user has downloaded their file.
' Then save a cookie on their machine and make
' a note in your database. In any future attempts,
' you can check for the cookie to ensure that the user can ONLY download
' the file to the same machine. That way you can prevent
' them from giving their login details to others who could then
' download the file from their machines.
If UserIsValid() Then
' Great. The user is valid and we're happy to give
' them the file. Now we open our file using the file stream object
fs = File.Open(strPath & strFileName, FileMode.Open)
' Declare a byte array to hold our stream information.
' Note, we initialize the byte array to the file stream's size
Dim bytBytes(fs.Length) As Byte
' Write the stream to the byte array
fs.Read(bytBytes, 0, fs.Length)
' Close the file stream to release the resource
' This is important. If you don't close the resource,
' the next person trying to download your file
' may get an error saying the resource
' is still being used by another application.
fs.Close()
' Next we need to add some header information.
' These headers will tell the browser what it needs to do
' with the content we're serving
' The first header ensures that the file name
' is correct on the client side. This is extremely important! If you
' don't add this header, the browser will make
' some sort of arbitrary decision as to what the file should be called.
' It will either offer no name and force the user to select a name
' (As we all know, this is like giving a two-year-old a loaded gun)
' or it will simply name the file after the web page
' it's calling (eg. MyFileServer.aspx). This will save without any problem
' but when the user tries to open the file, their system
' will tell them it doesn't know what to do with a *.aspx file
' and you're going to slowly go insane over the next
' six months with support calls, trying to explain to users how to
' change the file extension from *.aspx to *.pdf on their system
Response.AddHeader("Content-disposition", _
"attachment; filename=" & strFileName)
' Next, we need to tell the browser what type of content
' we're serving. If we don't add this header, the user's browser
' will assume it is standard html and try to render your
' bytes as text. The page won't crash,
' but the user is going to see an unholy
' mess on their web page, with bundles of little blocks
' and funny faces that are absolutely meaningless to them.
' I'm using a standard application/octet-stream content type.
' It's better to find out exactly what MIME type your particular
' file extension is defined under, as this should produce
' better browser behaviour.
' However, for our purposes, this will work just fine
' This header tells the browser that it is serving
' an application file as a byte stream.
' The browser will know immediately
' that it shouldn't serve this file as text and will open
' the File Download box instead,
' in which it offers the user the ability to save
' the file. It will also use the Content-disposition header
' (see above) to auto-populate
' the Save File dialog box with the file's name
Response.ContentType = "application/octet-stream"
' Now our headers are added, we can serve the content.
' To do this, we use the BinaryWrite() method of the server object
' This successfully streams our external file to the user,
' despite the fact that the file doesn't exist
' anywhere inside the web application
Response.BinaryWrite(bytBytes)
' Call Response.End() so that no more
' content goes through to the client.
' The file has been downloaded,
' but if this method is not called, the page
' will continue downloading any remaining
' html/ text content and mess up the resulting stream.
' This method call ensures that the downloaded
' file doesn't end up corrupted with unwanted data
Response.End()
Else
' The validation function returned false.
' In other words, we don't want to allow this user access.
' Simply serve the user normal text/html
' informing them that they don't have access to the file
Response.Write("Sorry. You don't have access" & _
" to this file. Please return to the login page and try again.")
End If
Points of Interest
XP has some new security settings that may cause problems on the file download feature in browsers. You'll probably need some instructions for people with XP systems to follow. This is a short-term thing - and it's something they'll experience on any file download over the web. As users become accustomed to XP's settings and defaults, they should begin placing fewer support calls.
As a rule, I post the following under the FAQs of any site that uses the above code:
- Q. I can't download documents from the site. When I click the Download button, the screen pops up, then immediately disappears. Why won't the files download?
- A. You're probably using Windows XP, which has default settings that prevent you from downloading documents automatically. The only way around this problem is to disable XP's default browser settings and allow it to automatically download documents:
- In your IE browser, click Tools | Internet Options
- Go to the Security tab, then click the Custom Level button.
- Under the Downloads section of the list, look for Automatic prompting for file downloads and click Enable.
- Click OK.