Skip to page content or skip to Accesskey List.
Search evolt.org
evolt.org login: or register

Work

Main Page Content

Handling anonymous file uploads in ColdFusion.

Rated 4.04 (Ratings: 5) (Add your rating)

Log in to add a comment
(5 comments so far)

Want more?

 
Picture of Nepolon

Steve Lewis

Member info | Full bio

User since: May 01, 2002

Last login: November 27, 2006

Articles written: 1

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:

  1. 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.
  2. 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.

The enctype=&quot;multipart/form-data&quot; 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=&quot;file&quot; 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=&quot;...&quot; 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.

What is 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.

Now 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.

NOTE: 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.

Steve Lewis (Nepolon) is a lead developer for Lunar Logic. On good days he actually gets to write Java and XSL on company time. In the past he has worked in the development of CMSs (content management systems), fan websites, the online store for a small publishing organization, as a tech support goon, and a variety of other absurd jobs.

Working with the web since 1994, Steve can often be found mumbling to himself in languages such as SQL, ColdFusion, PHP, and Java and is recovering nicely from his experiences with ASP and PERL; on particularly frightening days he has been known to mutter in C and C++.

Note on destination directories and filenames

Submitted by Nepolon on August 17, 2002 - 12:27.

I did want to make a few notes about general security concerns with users uploading to your server. It is important that you isolate the user's uploaded files from any of your own files. This means you should have a dedicated upload destination directory (or multiple dedicated upload destination directories) so you can more easily keep your work and user content separate. Secondly, you should think about how comfortable you are letting users chose or influence the filename that will live on your server. What do you want the filenames to look like anyway? Think of this in terms of how the file will be retrieved as well. In this example I am prefixing the user's chosen filename with info of my chosing and trusting there is no risk in the remainder. At the very least, if I provide a link to this uploaded file somewhere I will need to use the URLEncodedFormat() on the filename before I output it.

login or register to post comments

Denial of service

Submitted by anj on August 28, 2002 - 04:38.

CF5 (perhaps MX as well, not sure) has a potential denial-of-service issue with the CFFILE tag. There's no way to limit acceptable downloads based on size, so a user could start session(s) uploading huge files to you. And the CFFILE tag will not only accept the information, but store the file in RAM until completely received. Hence, if I start a 20G file uploading to your site, I'm pretty much guaranteed to force your system to chew up all available RAM and probably all available swap space as well. This will most likely crash either your OS or your CF processes.

login or register to post comments

RE: denial of service

Submitted by thickpaddy on November 5, 2002 - 11:02.

This is an issue that can be dealt with using URLScan for IIS (see RequestLimits section of ini file) and the limitRequestBody directive for Apache. You can also deny requests with transfer-encoding headers using URLScan, not sure how to do same in apache.

login or register to post comments

Heads Up

Submitted by topper on January 15, 2003 - 06:08.

In the following snippet of code from code block 2...

<cfif ListFindNoCase(request.accept, request.filetype) OR
ListFindNoCase(request.badext, CFFile.ClientFileExt)>

should read...

<cfif ListFindNoCase(request.accept, request.filetype) AND NOT
ListFindNoCase(request.badext, CFFile.ClientFileExt)>

or else i'm nuts! As is, its saying... if this is an accepted flietype OR an extension WE DON'T WANT then let it through...

Maybe i'm just missing someting. Best Regards, Topper

login or register to post comments

Good Catch, topper

Submitted by Nepolon on June 3, 2004 - 12:06.

Thanks for the correction topper. I have amended the article to reflect your AND NOT rather than my original OR

I also received the following query by email:
I was wondering why you would specify a list of unacceptable extensions AND specify which formats are acceptable. I was also wondering why you didn't use the "accept" attribute of the cffile tag, instead creating your own list separately. And lastly, why you want to put files in a temp directory initially... if you use the "accept" attribute of cffile, and someone tries to upload a document that isn't listed there, the document never uploads anyway, you just get an error message.
These are good questions. My answer, in summary, is that I recommend two distinct checks:
  • The mime-type is acceptable (this is what the "accept" attribute of the cffile tag does)
  • The file extention is acceptable

The HTTP request containing the file to be uploaded is responsible for telling us what mime-type to use. On the other hand, most web servers use file extensions to determine how to deliver requested files. If someone wants to pretend that security_breach.cfm is a image/gif they can do it, and I won't know. The web server will deliver the file to ColdFusion when a user requests to see the file, based on the file extension. This is bad: someone is now executing arbitrary ColdFusion code on your server.

Why do I bother duplicating the functionality of the accept attribute? I didn't want to use a separate try/catch block to detect invalid form submissions. By doing the check explicitly we complicate our code, but we also have full control over how to handle the error without resorting to an expensive error-handling routine. Use the accept attribute if you like, and remove the corresponding check of mime-types. Do not remove the check for file extensions, however.

Finally, I use the temp directory, a directory outside the web root and thus inaccessable to HTTP requests, to avoid a possible race condition: If I drop the uploaded file in the final destination, then run the checks on the extension (and mime type), the file is publicly accessible even if only for a moment. I don't want to take any risks that I can avoid, so I don't put unacceptable uploads where they can be found. Even if the action="delete" fails on a forged-mime-typed CFM file, the CFM file can never be executed.

I hope this clarifies some security points that I failed to detail deeply enough in the original article.

login or register to post comments

The access keys for this page are: ALT (Control on a Mac) plus:

evolt.orgEvolt.org is an all-volunteer resource for web developers made up of a discussion list, a browser archive, and member-submitted articles. This article is the property of its author, please do not redistribute or use elsewhere without checking with the author.