Many web applications I've worked on have had a requirement along the following lines:
- A user should be able to upload a file from their computer, and other users of the site should be able to download and view these documents.
The specifics differ from project to project. For some projects, only certain users can upload files. In others, only certain subsets of users can view the uploaded files. In some cases, only authenticated users can view the files. In others, anyone can upload any file and these files are viewable by everyone.
When faced with such a task there are two design decisions that need to be made up front:
- How are the uploads going to be stored?
- How are the files going to be downloaded?
The uploaded files can be stored directly in the database or on the web server's file system. I've used both approaches in several projects, and the two ways each have their own pros and cons. I discuss the benefits and demerits of storing binary data in the file system versus the database in the four Working with Binary Data tutorials available from my Working with Data in ASP.NET 2.0 tutorial series.
If you choose to store files on the file system, then these files can be downloaded in one of two ways: by pointing users to the web-accessible folder the uploaded files reside in, or by creating an ASP.NET web page that takes in as a querystring parameter the name of the file to download and then streams the file to the visitor's browser. The second option is a must if you need to apply any server-side logic or actions before a file is downloaded. For example, if only certain users can download a file, then you would want to use the second option so that you can first ensure that the current user can view the requested file. Alternatively, you may want to log all file downloads or perform some other sort of bookkeeping, in which case having an ASP.NET page to process downloads is advantageous over sending the user directly to the file to download.
In a recent project we wanted to allow all users to download any files. In short, we had a page where any authenticated user (company employees) could upload company wide documents (like PDF press releases and whatnot) and any visitor - authenticated or not - could download the documents. For this model, it seemed to make sense to just have the files uploaded to a web-accessible folder. Then, on the page that listed the documents, there would be a link pointing directly to the PDF file (or whatever), like http://intranet/files/OctoberProfits.pdf.
Simple enough, eh? But can you see the security hole? What if a disgruntled employee created an ASP.NET page with inline code that deleted all of the files on the web server? He could then unleash his wrath by visiting http://intranet/files/DeleteItAll.aspx. Or, worse yet, the disgruntled employee might write an ASP.NET page that connects to the HR database and displays salary information. Whoops!
In short, whenever you allow a user to upload a document that is saved to a web accessible folder, and then you allow others to visit that document directly through their browser, there exists the possibility that a nefarious user will upload a script file so that they can execute code when the file is visited through a broweser. The best way to prevent this is to configure IIS so as not to allow script execution on the folder (and subfolders) where files are uploaded. That is, set aside a single folder where all documents will be uploaded, and then configure IIS to disable script execution on that folder.
In IIS 6, you can disable script execution by going into the IIS Manager and drilling down to the appropriate folder. Right-click the folder and choose Properties from the context menu. From the Directory tab, change the “Execute permissions” setting from “Scripts only” to “None”, as shown in the screen shot below.
With this change in place, if someone uploads a script file (such as a .asp or .aspx file) and then they (or another visitor) visits it through their browser, IIS will return an HTTP 403.1 error - Forbidden: Execute access is denied. Security hole plugged!