Skip to Content
Course content

68: Secure File Upload Handling

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to show you a snippet of code that looks perfectly reasonable at first glance. I've seen this exact pattern in countless junior developers' portfolios, and it's one of the most dangerous mistakes you can make in a PHP application.

// The "Naive" Upload Script
$target_dir = "uploads/";
$filename = $_FILES["user_file"]["name"]; 
$target_file = $target_dir . basename($filename);

if (move_uploaded_file($_FILES["user_file"]["tmp_name"], $target_file)) {
    echo "The file has been uploaded!";
} else {
    echo "Sorry, there was an error uploading your file.";
}

If you're thinking, "Wait, isn't basename() enough to stop path traversal?" you're halfway there, but you're missing the bigger picture. The problem here isn't just where the file goes—it's what the file is. By trusting $_FILES["user_file"]["name"], you are letting the user decide what the file is called and, more importantly, what extension it has.

The Danger of Trusting User-Provided Filenames

Imagine I'm an attacker. I don't upload vacation_photo.jpg. Instead, I upload a file called cmd.php containing a simple payload: . Because your script blindly accepts the filename and moves it into a web-accessible directory, I now have a "web shell" on your server. I can navigate to yourdomain.com/uploads/cmd.php?cmd=ls -la and see every file in your directory. I've just gone from a visitor to having remote code execution (RCE).

Even if you try to check the extension with something like pathinfo(), attackers can bypass that with double extensions (like image.jpg.php) or null byte injections in older PHP versions. You simply cannot trust the client-side metadata.

Sanitizing and Renaming for Safety

The first rule of secure uploads is: Never use the original filename. I don't care if the user wants their file to be named "My Resume.pdf"; you should rename it to something the system generates. This kills two birds with one stone: it prevents RCE by stripping the extension and it stops users from accidentally overwriting each other's files.

// A much safer approach to naming
$target_dir = "uploads/";
$extension = pathinfo($_FILES["user_file"]["name"], PATHINFO_EXTENSION);

// Generate a cryptographically secure random name
$new_filename = bin2hex(random_bytes(16)) . '.' . $extension;
$target_file = $target_dir . $new_filename;

Now, we've solved the naming problem, but we still have a problem: I could still upload a PHP file and just rename it to randomstring.php. We need to validate the actual content of the file.

Validating Content, Not Just Extensions

Checking the extension is like checking the label on a soup can without opening the can. To actually know what's inside, we use the finfo (File Information) extension. This looks at the "magic bytes" at the beginning of the file to determine its actual MIME type.

$allowed_types = ['image/jpeg', 'image/png', 'application/pdf'];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime_type = $finfo->file($_FILES["user_file"]["tmp_name"]);

if (!in_array($mime_type, $allowed_types)) {
    die("Error: Invalid file type. Please upload a JPG, PNG, or PDF.");
}

// Only now do we move the file
if (move_uploaded_file($_FILES["user_file"]["tmp_name"], $target_file)) {
    echo "File uploaded securely.";
}

I'll give you one last pro tip: if you can, store your uploads outside the web root (e.g., one level above public_html). If the files aren't in a folder the web server is configured to serve, they can't be executed as scripts even if an attacker manages to sneak a .php file past your filters. You can then use a separate PHP script to read the file and stream it to the user using readfile().




📋 Practical Task

Build a Secure Profile Picture Uploader

Create a PHP script that allows a user to upload a profile picture. Your implementation must satisfy the following security requirements:

  • Strict MIME Validation: Use finfo to ensure the file is specifically an image/jpeg or image/png. Reject all other types.
  • Filename Randomization: The file must be saved using a randomly generated string (e.g., via bin2hex(random_bytes())) so the original filename is completely discarded.
  • Size Limitation: Check $_FILES["user_file"]["size"] and reject any file larger than 2MB.
  • Directory Protection: Create an uploads/ folder and include a .htaccess file inside it that disables PHP execution (php_flag engine off) as an extra layer of defense.

Test your script by attempting to upload a .txt file renamed to .jpg to see if your finfo check catches the deception.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.