Uploading Files With ColdFusion
Feb 18 |
Uploading files to the server is a critical function of any modern web server, however, there can be some severe security implications if your logic is not properly implemented. In this article, we will discuss some of the critical steps that should be performed when using ColdFusion to upload files and will discuss the different upload functionality when using ColdFusion's cffile.
Additionally, I will cover other sources of information on this topic and hope to impart a few new nuggets of information that I have learned in the last decade. At the end of this article, it is my goal that you should have enough knowledge to implement a working example of code.
Table of Contents
Understanding How the Cffile Tag is Used to Upload Files
The Cffile tag is used to manipulate files on the server and it is important to understand a few key concepts when using it to upload files.
Differences Between cffile action="upload" and cffile action="uploadAll"
Both the uploadAll and upload methods are similar, however, there are some fundamental differences between the two different methods.
The uploadAll method will upload either one or more files to the server at the same time. When sending multiple files, this often leads to increased performance as it avoids having to make multiple cfupload calls. Additionally, there is no performance penalty if you only send a single file.
However, when using uploadAll, the order of the file upload is not guaranteed. Also, there is no granular control as ColdFusion will regenerate the result array of structures with every new file uploaded. To preserve the sort order that the user chose when uploading the files, you should choose to send the files sequentially using multiple cffile.cfupload calls.
Due to the potential performance increase, I tend to use the uploadAll method unless I need more granular control or if the order of the file uploads is important.
Status Parameters
After the image has been uploaded, there is a wide variety of status parameters. The status values that we are using in this example are:
- serverFile (the name of the file)
- serverDirectory (the directory where the file is held)
- contentType (the mime type of the file)
- fileSize
- serverFileExt
for a full list of these cffile status parameters see https://helpx.adobe.com/coldfusion/cfml-reference/reserved-words-and-variables/coldfusion-tag-specific-variables/cffile-action-upload-variables.html
How the Upload Methods Return Status Parameters
The other fundamental difference is how the methods return these values. uploadAll will return the results of the file operations in an array of structures, whereas the upload method will return the values as a structure.
To obtain the return values when using action="upload" simply use the the key-value pair like so: file.serverFile.
When using uploadAll, you will need to loop through the outer array to get the values of the structure like so:
<cfloop array="#UploadObj#" item="image">
<cfoutput>#image.ServerFile#</cfoutput>
</cfloop>
In the example below, I have adapted the code to handle both upload and uploadAll.
The fileField Argument is Optional and Should not be Used
Cffile will automatically inject files and there is no reason to use the fileField argument. Using the fileField argument will limit your flexibility and may cause errors.
There are many client-side JavaScript libraries used for uploading files and they all seem to have different names for their form controls. Also, many of them use names that are not compatible with ColdFusion, for example, Uppy uses files[] as the form name and this will cause a ColdFusion error if you try to use it.
Do not Rely Upon the Accept Attribute
The cffile accept attribute is used to limit what mime types are accepted by the server. However, it is not bulletproof. According to Pete Freitag's Tips for Secure File Uploads with ColdFusion article, the browser informs the server what the mime type is, and "It's very easy to spoof the mime type".
The accept attribute should be used, but it is also crucial to inspect the file. In this working example, I am using the accept argument, but additionally, I am using the isImageFile method along with checking to content type in the temp directory before moving the file to the final destination. However, according to Pete Frietag, the isImageFile may not be reliable on older versions of ColdFusion (CF8 in particular).
Always Upload Files Outside of the Web Root
It is critical to initially upload the files to a temporary destination outside of the web root. Bad actors can upload malicious files to potentially exploit or crash your server if you allow them to upload directly to the web root. For extra security on public-facing sites, you may want to upload the images on a dedicated remote server.
Limit the Size of the Files in the ColdFusion Administrator
In our example below, we are checking to see if the file size is larger than 50k before we proceed to move the file from the temp directory to its final destination. The problem with this approach is that we must wait until the file is uploaded before we can determine any of the file properties. Theoretically, a bad actor can potentially crash your site by uploading a bunch of huge files.
To alleviate this concern, it is also important to limit the size of the files by adjusting ColdFusion's Size Limit Settings in the ColdFusion Administrator.
Pete Freitag recommends setting the Maximum Post Data to 100 MB, setting the Request Throttle Threshold to 4 MB, and setting the Request Throttle Memory to 200 MB. See https://www.petefreitag.com/blog/secure-file-uploads-coldfusion/ for more information.
ServerFault has also captured an interesting StackExchange discussion regarding the effects of these settings- see ColdFusion settings for large file uploads
Upload Process Methodology
Our working example below uses the following steps:
- Create a list of accepted file types that will be accepted by the cffile tag
- Initially upload the files to a sandbox or into an isolated directory outside of the web root, such as the tempDirectory. By default, we will use cffile action="uploadAll" to upload the files unless the forceSequentialUpload argument is set to true.
- Catch upload errors
- Once the file(s) are uploaded, we will check the file size and verify that the file is an image based on the extension and content type. If the content type does not match, or if the size of the image is too large, we will delete the image from the temp directory.
- If the file is under the desired size limit, and it is an image, move the image to its final destination
- Return data to the server
Code
The following code has been slightly adapted from my production code. The code is documented well and should speak for itself. Feel free to comment if you have any questions.
<cffunction name="uploadImage" access="remote" output="true" returnformat="json"
hint="This function uploads an image. If the updates were successful, it returns an empty json array.">
<cfargument name="mediaProcessType" type="string" default="enclosure" required="false" hint="What media files are we processing? This string determines how to process the image and potentially save it to the database. In my code, valid options are enclosure, gallery and carousel.">
<cfargument name="uploadDirectoryUrl" type="string" default="/demo/images/" required="false" hint="Specify the destination folder. Use forward slashes before and after the directory (ie /site/images/)">
<cfargument name="forceSequentialUpload" default="false" required="false" hint="When set to true, each image is uploaded sequentially. This is important when uploading images for a carousel or a gallery when you don't want the images out of order.">
<!--- Error params --->
<cfparam name="error" default="false" type="boolean">
<cfparam name="errorMessage" default="" type="string">
<!--- Note: this function follows the best practices given by http://learncfinaweek.com/course/index/section/Security/item/File_Uploads/. --->
<!--- There are four main processes here- uploading the image, error handling, saving the data to the database, and returning data back to the client..
A Upload the original image to a temp directory
B Check for sanity, and move the image to the proper folder.
C Return the data to the client. --->
<!--- ************************************ A Inspect images and upload them ************************************ --->
<!--- Allowed mime types. --->
<cfset acceptedMimeTypes = {
'image/jpeg': {extension: 'jpg'},
'image/gif': {extension: 'gif'},
'image/webp': {extension: 'webp'},
'image/png': {extension: 'png'}
}>
<!--- ******************************* Upload the image(s) to a temp directory ******************************* --->
<!--- Here we are uploading all of the files to ColdFusion's temporary directory and then check the file(s) before we upload them to our permanent destination. The file field name may vary here, its different for the uppy (ie. files[]) and tinymce (ie file) interfaces, and we need some extra logic to differentiate them. --->
<!--- Put this in a catch block --->
<cftry>
<cfif forceSequentialUpload>
<!--- When we are uploading for a gallery or carousel, we need to make sure to upload the files sequentially. We could simply use cffile action="uploadAll", however, that would upload all of the files at the same time and would not preserve the order that the user chose when uploading. Note: we are not using the fileField argument. It is not necessary. Cffile will automatically understand that an image is sent --->
<cffile
action="upload"
accept="#structKeyList(acceptedMimeTypes)#"
strict="true"
destination="#getTempDirectory()#"
nameconflict="overwrite"
result="file">
<!--- When using action="upload", there is one file. However, using action="uploadAll" (with forceSequentialUpload = false), there is a structure of files. Since we are using both methods, I need to push this file into a new struct with a single item to keep the logic intact for both branches (ie upload and uploadAll) --->
<cfscript>
UploadStruct=structNew();
UploadStruct.ServerFile=file.ServerFile;
UploadStruct.ServerDirectory=file.ServerDirectory;
UploadStruct.FileSize=file.FileSize;
</cfscript>
<!--- Push the new structure inside of an array containing on row. --->
<cfset UploadObj=arrayNew(1)>
<cfset UploadObj[1]=UploadStruct>
<cfelse><!---<cfif forceSequentialUpload> --->
<!--- If you don't need to keep the order intact, use uploadAll without a fileField argument. This is generally more efficient if there are multple files as it avoids multiple calls and uploads everything at once. --->
<cffile
action="uploadAll"
accept="#structKeyList(acceptedMimeTypes)#"
strict="true"
destination="#getTempDirectory()#"
nameconflict="overwrite"
result="UploadObj">
</cfif><!---<cfif forceSequentialUpload> --->
<!--- **************************************** B Error handling **************************************** --->
<!--- Catch any errors --->
<cfcatch type="any">
<!--- Note: files are not written to disk if error is thrown --->
<!--- Set our error flag --->
<cfset error = true>
<!--- Prevent zero length files --->
<cfif findNoCase( "No data was received in the uploaded", cfcatch.message )>
<cfset errorMessage = errorMessage & "<li>Zero length file</li>">
<!--- Prevent invalid file types --->
<cfelseif findNoCase( "No data was received in the uploaded", cfcatch.message )>
<cfset errorMessage = errorMessage & "<li>The MIME type or the Extension of the uploaded file</li>">
<!--- Prevent empty form field --->
<cfelseif findNoCase( "did not contain a file.", cfcatch.message )>
<cfset errorMessage = errorMessage & "<li>Empty form</li>">
<!--- Catch all other errors --->
<cfelse>
<cfset error = true>
<cfset errorMessage = errorMessage & "<li>Unhandled File Upload Error: #cfcatch.message#</li>">
</cfif>
</cfcatch>
</cftry>
<!--- B If there are no errors, move the image to the desired destination and return the image path info --->
<cfif not error>
<!--- There can be one or more images. Loop through the image array provided by cffile --->
<cfloop array="#UploadObj#" item="image">
<!--- After the file is uploaded, we will have access to the file size and other file-related properties. Since this particular function is developed to upload images, we want to allow a larger size than a typical file and have set the maximum size of 150k. The fileSize returns the image size in bytes and there are 1024 bytes in a megabyte. We are also not relying upon the mime type sent by the server and double-checking to see if this is indeed an image. --->
<cfif ( not isImageFile(getTempDirectory() & image.serverFile) or (image.contentType neq "image")
or (image.fileSize gte (150 * 1024) ) ) >
<!--- Delete the file --->
<cffile
action = "delete"
file = "#getTempDirectory()##image.ServerFile#">
<!--- Set our error flag --->
<cfset error = true>
<cfset response[ "errorMessage" ] = "The image must be under 150k. Please choose a smaller file">
<cfreturn serializeJson(response)>
<cfelse><!---<cfif (image.fileSize gte (150 * 1024))>--->
<!--- Set our final destination. To reduce potential conflict between the image names, we are saving each type of media in its own folder --->
<cfset destinationFolder = application.baseUrl & uploadDirectoryUrl>
<!--- Get the file path --->
<cfset destination = expandPath(destinationFolder)>
<!--- Set the URL. --->
<cfset imageUrl = application.baseUrl & destinationFolder & image.serverFile>
<!--- Move the file to the final destination. --->
<cffile
action="move"
source="#image.serverDirectory#/#image.serverFile#"
destination="#destination#"
mode="644">
<!--- Save images to the database --->
<!--- Use your own code here --->
<!--- D Return data to the client. --->
<!--- Create a new location struct with the new image URL --->
<!--- Note: this no longer works in CF2021 (it works in prior versions to CF11). It returns the keys in upper case:
<cfset imageUrlString = { location="#imageUrl#" }>
--->
<cfset imageUrlString["location"] = "#imageUrl#">
<!--- Return the structure with the image back to the client --->
<cfreturn serializeJson(imageUrlString)>
</cfif><!---<cfif ( not isImageFile(getTempDirectory() & image.serverFile) or (image.contentType neq "image")
or (image.fileSize gte (150 * 1024) ) ) > --->
</cfloop><!---<cfloop array="#UploadObj#" item="image">--->
<cfelse>
<!--- Serialize our error list --->
<cfset response[ "errorMessage" ] = "<ul>" & errorMessage & "</ul>" />
<!--- Send the response back to the client. This is a custom function in the jsonArray.cfc template. --->
<cfreturn serializeJSON( response )>
</cfif>
</cffunction>
Further Reading
- Tips for Secure File Uploads with ColdFusion (Pete Freitag's authoritative article on this subject, is a must-read!)
- File Upload Guide (Raymond Camden)
- ColdFusion, jQuery, And "AJAX" File Upload Demo (Ben Nadel)
- Security File Uploads Learn CF in a week)
- Ask Ben: Limit File Upload Size In ColdFusion
Article updated on March 11, 2024
Related Entries
Tags
ColdFusion File UploadsThis entry was posted on February 18, 2024 at 6:37 PM and has received 1139 views.