DevLearningTools

MODULE 11 · LESSON 02

Uploading Files

Handling a form file upload with cffile action="upload" and fileUpload(), the full result struct, and the real security gotcha that MIME types can be spoofed so extensions need server-side validation too.

New lessons are added one at a time as the course gets built out — a graded quiz for each lesson is still on the way.

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

HTML — the form
<form method="post" enctype="multipart/form-data">
    <input type="file" name="photo">
    <button type="submit">Upload</button>
</form>
Tag Syntax
<cffile action="upload"
    destination="#expandPath('./uploads/')#"
    filefield="photo"
    accept="image/jpeg,image/png"
    nameconflict="makeunique">
CFScript
result = fileUpload(
    expandPath("./uploads/"),
    "photo",
    "image/jpeg,image/png",
    "makeunique"
);
NOTE

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

FieldMeaning
clientFile / clientFileName / clientFileExtThe original filename (and its parts) from the uploader's own computer
serverFile / serverFileName / serverFileExtThe filename actually used on the server, which may differ if renamed
serverDirectoryWhere the file was actually saved
contentType / contentSubtypeThe general MIME category and specific subtype, e.g. image and png
fileSizeUpload size in bytes
fileWasSavedWhether the file was actually stored
fileWasRenamedWhether the filename was changed during saving (e.g. by makeunique)
fileExisted / fileWasOverwrittenWhether a file already occupied that destination, and whether it got replaced
NOTE

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.

CFScript — validating the actual extension too
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);
}
NOTE

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 valueBehavior
errorThe upload fails if a file with that name already exists (the default)
skipThe existing file is left untouched, the new upload is discarded
overwriteThe existing file is replaced
makeuniqueThe 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.