Calling CFFile-Upload Twice On The Same File For Security Purposes
One of the things that I have always loved about ColdFusion's CFFile upload processing is the seamless way in which ColdFusion handles name conflicts. Simply set the "nameconflict" attribute to "makeunique" and you don't have to worry about anything else. Except maybe security. Luckily, I just discovered that ColdFusion allows us to call the CFFile-Upload action more than once on the same file within a single request. This allows us to upload a file to multiple destinations, taking independent advantage of both upload validation and ColdFusion's conflict resolution.
When we process file uploads, we typically use some variation on the following workflow:
- Save the upload to disk (on the server).
- Validate the file contents (or file extension).
- Integrate the file into our application.
Unfortunately, if the file validation is done in a publicly-accessible folder (ie. under the web root), it can expose a serious security threat. Even if the validation takes place milliseconds after the file has been written to disk, a load-tester can use hundreds of simultaneous requests in order to execute a malicious upload in the clock-ticks between step 1 and step 2 above.
To prevent this from happening, you have to upload files to an intermediary, non-public directory for validation before integrating the file into your app. However, you probably don't want your post-validation file processing to have to worry about moving the file around - this is just an unnecessary complication. Fortunately, you can keep these two steps fairly independent by calling the CFFile-Upload action twice on the same file.
In the following demo, notice that I am calling CFFile-upload once to get the file into a secure location for validation; then, once it is validated, I am calling the CFFile-Upload action a second time in order to complete the file integration.
<!--- Param the form fields for the submit. --->
<cfparam name="form.submitted" type="boolean" default="false" />
<cfparam name="form.file" type="string" default="" />
<!--- I will hold errors generated during the upload processing. --->
<cfset errorMessage = "" />
<!--- I will hold the name of the uploaded file (if successful). --->
<cfset imagePath = "" />
<!--- Set the path for the uploads folder. --->
<cfset uploadDirectory = expandPath( "./uploads/" ) />
<!---
Set the path for our temporary directory - this is the
intermediary directory where will upload files that need to be
evaluated for safety.
NOTE: This should NOT be a web-accessible directory (however in
our demo, it is).
--->
<cfset tempDirectory = expandPath( "./secure/" ) />
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Check to see if the form has been submitted. --->
<cfif form.submitted>
<cftry>
<!--- Make sure a file was selected. --->
<cfif !len( form.file )>
<cfthrow
type="FileNotFound"
message="Please select a file to upload."
/>
</cfif>
<!---
Make sure the file uploaded was actually an image. To do
this, we have to save the file to disk; however, we want
to make sure not to write to a place where an attacker
can stage a load-based attack on our file validation.
Let's use CFFILE to upload the file to the secure and
quarantined temp directory.
--->
<cffile
result="upload"
action="upload"
filefield="file"
destination="#tempDirectory#"
nameconflict="makeunique"
/>
<!--- Check if its an image file supported by ColdFusion. --->
<cfset uploadIsNotImage = !isImageFile( "#tempDirectory##upload.serverFile#" ) />
<!--- Delete the file now that we've validated it. --->
<cfset fileDelete( "#tempDirectory##upload.serverFile#" ) />
<!---
Check to see if we should continue processing. If the
file is not an image, there's nothing more to do.
--->
<cfif uploadIsNotImage>
<!--- ALERT: Possible Attack!! --->
<cfthrow
type="InvalidImageFile"
message="Please select a valid image file."
/>
</cfif>
<!---
If we've made it this far, then we know the user has
selected a valid image file. Now, we want to move the
file into the uploads directory. Let's use the CFFile
tag AGAIN in order to let ColdFusion handle the file
conflict resolution.
--->
<cffile
result="upload"
action="upload"
filefield="file"
destination="#uploadDirectory#"
nameconflict="makeunique"
/>
<!---
Set the path of the image so we can display it back
to the user.
--->
<cfset imagePath = "./uploads/#upload.serverFile#" />
<!--- Catch any upload processing errors. --->
<cfcatch>
<!--- Set the error message for the user. --->
<cfset errorMessage = cfcatch.message />
</cfcatch>
</cftry>
</cfif>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Reset the output buffer. --->
<cfcontent type="text/html; charset=utf-8" />
<cfoutput>
<!doctype html>
<html>
<head>
<title></title>
</head>
<body>
<h1>
Upload An Image
</h1>
<!--- Check to see if we have an error message to display. --->
<cfif len( errorMessage )>
<p>
<strong>Ooops:</strong> <em>#errorMessage#</em>
</p>
</cfif>
<form
method="post"
action="#cgi.script_name#"
enctype="multipart/form-data">
<input type="hidden" name="submitted" value="true" />
<p>
Please select an Image to upload:<br />
</p>
<p>
<input type="file" name="file" size="30" />
<input type="submit" value="Upload Image" />
</p>
</form>
<!---
Check to see if we have an uploaded image to display
back to the user.
--->
<cfif len( imagePath )>
<h3>
Your Upload:
</h3>
<p>
<img src="#imagePath#" width="400" />
</p>
</cfif>
</body>
</html>
</cfoutput>
By using this kind of approach, we can add file-based validation without complicating the rest of the upload and processing. Notice that by using the CFFile-Upload action a second time, we can still leverage the "nameconflict" resolution (ie. "makeunique") feature provided by ColdFusion.
I used to think that when you called the CFFile-Upload action, ColdFusion would actually move the TMP file out of the server's temporary directory and into the upload destination. Only yesterday did I discover that this was not true. The temporary directory, from my understanding, is cleared out periodically; however, having the TMP file available for multiple CFFile-Upload calls within the same request has some really awesome benefits.
Want to use code from this post? Check out the license.
Reader Comments
Ben,
In CF10, two attributes - accept and strict have been added that will allow you to restrict the type of file being uploaded. You can provide MIME types or file extensions as a value in the 'accept' attribute. If 'strict' is true then the content of the file is also validated.
I had posted this sometime back http://www.sagarganatra.com/2012/03/coldfusion-10-cffile-restricting-file.html
Let me know if you find this useful.
Sagar.
@Sagar,
It's definitely going to be useful! Especially if the Accept attribute can contain (depending on mode) file extensions and/or mime-types. Very cool!
@Ben,
Yes, cffile action="upload" is essentially a write operation, so you can do more than one.
Once upon a time, I wrote a multipart/form-data handler in C. (Not C++. Good ol' C.) So I know how it works at the HTTP level. The entire file is in memory at the time you decide to do something with it.
So that begs the question, why doesn't cffile action="write" have a nameConflict attribute? When you think about it, it kinda should.
Of course, in either case, upload or write, you could always do a FileExists call. I guess Allaire just went with the odds that you're more likely not to know whether the same file name already exists in the case of an upload.
Why exactly would you need the file in a specific location? Since it is already in memory you can just call
directly. Or are there functions you won't be able to call on it like this?
@WebManWalking,
I would actually love to have name-conflict resolution in a File-Write call. It's one of those things that you don't often need; but, when you do, it would be sweet!
@Ralph,
Ah, awesome suggestion! In this case, that works perfectly. I'm working right now on factoring this out a bit to use file metaData instead of the file content - just a different take on it.
@Ralph,
Actually, I just tried that and it seems to fail (at least in ColdFusion 8). I guess since the file has a ".tmp" extension at that point, ColdFusion doesn't see it as a valid image file extension. I guess, for performance reasons, ColdFusion doesn't actually look at the file content.
@All,
I've refactored this concept out into its own utility function:
www.bennadel.com/blog/2399-Getting-The-MetaData-For-A-File-Upload-In-ColdFusion.htm
... this time, looking at a subset of the meta-data exposed by the natural CFFile-Upload action.
@Ben,
Difference is probably that I'm using Railo than :)
@Ralph,
Ha ha, Railo always trying to stay a few steps ahead in some cool areas!