A file upload starts with an HTML form submitted as multipart/form-data, the encoding needed to actually carry binary file data alongside regular fields. On the CFML side, cffile action="upload" (or its CFScript function equivalent, fileUpload()) reads that submitted file and saves it to a real location on the server.
Learning Objectives
After completing this lesson, you'll be able to:
- Handle a form file upload with cffile action="upload" and with fileUpload().
- Read the full result struct an upload returns to confirm what actually happened.
- Avoid a real security gotcha: MIME types can be spoofed, extensions need server-side validation too.
The Upload Flow
HTML Form
multipart/form-data
cffile / fileUpload()
Server Directory
Result Struct
A Basic Upload
<form method="post" enctype="multipart/form-data">
<input type="file" name="photo">
<button type="submit">Upload</button>
</form><cffile action="upload"
destination="#expandPath('./uploads/')#"
filefield="photo"
accept="image/jpeg,image/png"
nameconflict="makeunique">result = fileUpload(
expandPath("./uploads/"),
"photo",
"image/jpeg,image/png",
"makeunique"
);enctype="multipart/form-data" on the form isn't optional, without it the file's actual binary content never gets submitted at all, only its filename as plain text.
The Full Result Struct
| Field | Meaning |
|---|---|
| clientFile / clientFileName / clientFileExt | The original filename (and its parts) from the uploader's own computer |
| serverFile / serverFileName / serverFileExt | The filename actually used on the server, which may differ if renamed |
| serverDirectory | Where the file was actually saved |
| contentType / contentSubtype | The general MIME category and specific subtype, e.g. image and png |
| fileSize | Upload size in bytes |
| fileWasSaved | Whether the file was actually stored |
| fileWasRenamed | Whether the filename was changed during saving (e.g. by makeunique) |
| fileExisted / fileWasOverwritten | Whether a file already occupied that destination, and whether it got replaced |
Checking fileWasSaved (and the actual serverFile name) after an upload is what confirms the file genuinely landed where expected, rather than assuming success just because no error was thrown.
A Real Gotcha: MIME Types Can Be Spoofed
The accept attribute filters by MIME type, but a MIME type is just a label the browser sends, one a user (or a malicious script) can set to anything regardless of the file's actual content. Relying on accept alone isn't real validation.
try {
uploaded = fileUpload(
getTempDirectory(),
"photo",
"image/jpeg,image/pjpeg",
"makeunique"
);
if (!listFindNoCase("jpg,jpeg", uploaded.serverFileExt)) {
throw("The uploaded file is not a valid JPG.");
}
writeOutput("Uploaded: " & uploaded.serverFile);
} catch (any e) {
writeOutput("Upload error: " & e.message);
}Checking serverFileExt against an explicit allow-list after the upload catches a file whose real extension doesn't match what its spoofed MIME type claimed, exactly the gap accept alone leaves open.
Handling a Name Conflict
| nameConflict value | Behavior |
|---|---|
| error | The upload fails if a file with that name already exists (the default) |
| skip | The existing file is left untouched, the new upload is discarded |
| overwrite | The existing file is replaced |
| makeunique | The new file is saved under an automatically generated unique name |
Common Beginner Mistakes
Forgetting enctype="multipart/form-data" on the form
Without it, the file's actual content is never submitted, only its filename as a plain text value, the upload silently fails to carry any real file data.
Trusting the accept attribute alone as file-type security
MIME type is a label the client sends, not a verified fact about the file's real content. Always cross-check the actual serverFileExt after upload too.
Assuming an upload succeeded just because no error was thrown
Check fileWasSaved on the result struct to confirm the file was genuinely stored, rather than assuming success from the absence of an exception alone.
Not planning for a naming conflict at all
The default nameConflict behavior (error) fails the whole upload if a file with that name already exists — a real, common occurrence with generic filenames like photo.jpg, worth an explicit strategy rather than the default.
Best Practices
- Validate both the accept MIME type and the actual resulting serverFileExt, MIME type alone isn't trustworthy.
- Use nameconflict="makeunique" for user-facing uploads where filename collisions are expected and shouldn't fail the whole request.
- Store uploaded files outside the web root, or otherwise ensure an uploaded file can't accidentally be executed as code.
- Check the result struct's fileWasSaved and serverFile after every upload rather than assuming success.
Interview Questions
Why does the HTML form need enctype="multipart/form-data" for a file upload to work?
Without it, the browser submits the file field as plain text (just the filename), not the file's actual binary content, so nothing usable ever reaches the server.
Why isn't the accept attribute alone sufficient file-type validation?
accept filters by MIME type, which is just a label the client sends and can be spoofed to claim any type regardless of the file's real content. The actual saved file's extension (serverFileExt) should be checked separately.
What does nameconflict="makeunique" do differently from the default?
The default (error) fails the upload entirely if a file with that name already exists at the destination. makeunique instead saves the new file under an automatically generated unique name, letting the upload succeed.
How do you confirm an upload actually succeeded, beyond just catching an exception?
Check the fileWasSaved field on the returned result struct, and the actual serverFile name, rather than assuming success purely from the absence of a thrown error.
Summary
In this lesson, you handled a file upload with cffile action="upload" and fileUpload(), read the full result struct to confirm what actually happened, handled naming conflicts, and covered the real gotcha that MIME types can be spoofed, meaning the actual saved file's extension needs server-side validation too.
What's Next?
The next lesson covers cfdirectory, listing, creating, and deleting directories on the server.