Handling Anonymous File Uploads In Coldfusion
Posted on 16 Aug 2002
by Steve Lewis (Nepolon)
Rated 4.05 (Ratings: 5)
- More articles in Code
File uploads to your web server can be a real pain in the security. Consider the obvious case of uploading an executable and trying to engineer a way to get you to execute it... scary. Consider also the unobvious case of uploading a ColdFusion script that executed malicious code using your web server.
When you allow file uploads you are essentially allowing someone to place arbitrary arrangements of 1s and 0s on your server's disk and hoping that the user doesn't do anything mean with them. I have heard this referred to as the rhythm method of security. I hear it works about as well.
I don't know about you, but I tend to have difficulty sleeping at night whenever I am forced to allow folks permission to upload something to my servers. This is most commonly a problem on my web servers. On web servers folks often want to be able to store images, maybe a Word document or some PDFs, or maybe even some Zip files for an indefinite period of time and presumably with the understanding that someone will link to that file in an HTML file, and then sometime later someone will follow that link.
In order to get my uninterrupted sleepy time, I like to do what I can to limit the chance that someone will abuse this permission to upload pooh to my web server. Lets define our risks. There are two distinct cases for file uploads:
- The sender is someone who has paid me for the right to personally put files on my web server. I know who this person is, and have assigned a scheme for authenticating this person's identity before the upload happens. If things go wrong I have this person's phone number, a valid email address, and maybe even a hostage credit card.
- The sender is visiting a website on my server, is an unauthorized and anonymous user. I don't know who to yell at when I find several gigabytes of bootlegged MP3s of the upcoming tribute album to Artis the Spoonman on my server.
Let us presume that we trust the authenticated users and ignore them for right now. We want to focus on the second case and try to eliminate some of the potential risks.
First how do we create a form where someone can upload a file?
<!--- Beginning for your file-upload form ---><form action="#BuildSelfURL()#" method="POST" name="MyForm" enctype="multipart/form-data"> <input type="file" name="document" size="35" class="ft" accept="text/plain,application/msword," "application/pdf,application/rtf,application/mspowerpoint," & "application/x-visio,application/excel,application/x-msexcel," & "application/x-compressed,application/x-zip-compressed," & "application/vnd.ms-excel,application/x-excel,application/zip"></form>
Here, I am using a UDF called
BuildSelfURL() which will write a correct destination for my form with appropriate query string elements. You should probably replace it with whatever URL will handle your input validation and processing if you don't have this sort of thing built yet.
enctype="multipart/form-data" in the form is essential. Don't ask why if you don't want me to explain the reason to you in mind numbing detail. As you may guess the
input type="file" is what really telling the browser to help your user to select a file from his/her file system, and to send this file to the server when he/she submits the form. The
accept="..." bit is nice because it can help some of your potential users select acceptable files to begin with. This is not reliable because it is not widely implemented, and secondly because you can never trust clients. Client-side error checking should be done as a convenience to the user only, never as a safeguard for anything.
So how do we determine if the uploaded file is malicious? We don't believe in achieving a secure computer system, short of unplugging the server and throwing it off the continental shelf. Since that won't help you secure your functional file upload system I will try to be practical and help you reduce the risk by trying to determine if a file is unlikely to be malicious and we let it in if it doesn't scare us too badly. Sound good? OK, here we go.
<!--- Now test that any uploaded files are of an acceptable format before we do any DB work. Presume we are working in a windows environment. Upload destination uses \ notation to identify a directory. --->
<!--- uploaded file is of a generic business document format ---><cfif Len(form.document)> <cfset request.badext ="cfml,cfm,asp,shtml,php,cgi"> <cfset request.accept ="text/plain,application/msword," & "application/pdf,application/rtf,application/mspowerpoint," & "application/x-visio,application/excel,application/x-msexcel," & "application/x-compressed,application/x-zip-compressed," & "application/vnd.ms-excel,application/x-excel,application/zip"> <cffile action="UPLOAD" filefield="form.document" destination="#request.uploadtemp#\" nameconflict="MAKEUNIQUE"> <cfset request.tmpfilename = CFFile.ServerFile> <cfset request.filetype = CFFile.ContentType & "/" & CFFile.ContentSubType> <cfif ListFindNoCase(request.accept, request.filetype) AND NOT ListFindNoCase(request.badext, CFFile.ClientFileExt)> <cfset request.clientfile = CFFile.ClientFile> <cfelse> <cffile action="DELETE" file="#request.uploadtemp#\#CFFile.ServerFile#"> <cfset request.errors.document = "Field: Document. The file " & "format provided (#CFFile.ContentType#/" & "#CFFile.ContentSubType#) is not allowed."> </cfif></cfif>
How do we decide what file extensions are bad? Any scripting system can be dangerous. Depending on what scripting languages your server offers and what permissions look like you may need to modify the list of bad file extensions. You should also probably only allow those document MIME types that you need to. Any MS Office format could be potentially dangerous for instance, depending on what DLLs live on your server and what bugs Microsoft has left behind.
request.uploadtemp anyway? This is a temp directory outside of the web root. This directory must be outside the web root to eliminate a race condition in our system. That means that we do not, even for a moment, allow unacceptable file types to live on our file system where an http request could find it.
Why do we
MAKEUNIQUE? Because we don't know how many folks are uploading files at any given time, trying to get through our defenses. Best to not let them clobber each other's files.
request.tmpfilename stores the filename of the uploaded file as it was stored in our temp directory. This will be different from the file name that the client used for the file if there was a conflict with another file in the temp directory. I prefer to preserve the user's file name, so we stored that in
request.clientfile as well.
At this point we have taken the file and placed it in this temp directory and looked at it's file type and file extension. If the type was known to be good, and the extension was not known to be bad, than we make a note of where we parked the file in our temp directory and what it was originally named. Otherwise, we delete the file and generate an error message.
request.errors is a structure I use to manage error messages on forms. This code segment does not include the
StructNew() that occurs elsewhere, but the later code depends on this.
When I am expecting an uploaded image I use a slightly different approach:
[...]<cfset accept="image/gif,image/jpeg,image/pjpeg,image/jpg,image/png"><cfset goodext="gif,jpg,jpeg,png">[...]ListFindNoCase(request.goodext, CFFile.ClientFileExt)>[...]
The changes should speak for themselves. Now we have this uploaded file that doesn't appear to be likely to harm us. How do we get the file to its permanent home?
<!--- Only continue if no errors have been generated by form validation ---><cfif NOT StructCount(errors)> <cftransaction> <!--- Generate DB record for this entry, used in producing file names ---> <cfquery name="insert" datasource="#request.dsource#" dbtype="#request.dbtype#"> INSERT INTO element (uploaded) VALUES (#Now()#) </cfquery> <cfquery name="getid" datasource="#request.dsource#" dbtype="#request.dbtype#"> SELECT MAX(id) AS id FROM element </cfquery>
<!--- Perform final file activity ---><cfif Len(request.clientfile)> <cfset request.filename="#getid.id##request.clientfile#"> <cffile action="MOVE" source="#request.uploadtemp#\#request.tmpfilename#" destination="#session.mainuploadpath#\element\#filename#" nameConflict="OVERWRITE"> </cfif> <!--- Update DB record ---> <cfif Len(request.filename)> <cfquery name="update" datasource="#request.dsource#" dbtype="#request.dbtype#"> UPDATE element SET document = '#request.filename#', WHERE id = '#getid.id#' </cfquery> <cfelse> <cfquery name="delete" datasource="#request.dsource#" dbtype="#request.dbtype#"> DELETE FROM element WHERE id = '#getid.id#' </cfquery> </cfif> </cftransaction></cfif>
I am inserting a record into my database to record this file upload. Put in whatever interesting data fields and content you like here.... you may have other form fields on the page and may want to store data about them too.
Then I get the (primary key & auto-incremented identity) id for that newly-inserted record.
Next I move the file to its permanent home. I use the record id and the client's chosen filename together here to store the file, that way two different people could upload two different files with the same file name, and they would not end up clobbering each other, plus it is easy to connect the files in your file system with the DB records later. Note that I chose to overwrite this time. This is the behavior I prefer, YMMV.
Finally, now that I know what the final file name of this uploaded document is, I can save that in the database so I update the record I just inserted. Sleep well.
As a final note, you should always use proper error handling techniques. In particular, there are a number of instances where
CFTRY/CFCATCH blocks should be used to trap potential errors. The error handling code has not been included in the code samples above for simplicity and clarity.