PHP
Secure file upload
Learn MIME checks, random names and storage separation for secure file upload.
Secure file upload principles
Secure upload rests on three principles: MIME check, random name, storage separation. The finfo class (FILEINFO_MIME_TYPE) reads the MIME from the file's real content; $_FILES['x']['type'] is an untrusted browser hint. The uploaded file gets a random name via bin2hex(random_bytes(16)); the original name is not kept.
The file lives outside the web root; downloads go through a script. The extension is bounded by an allowlist (png, jpg, pdf); in_array is called in strict mode (third argument true). move_uploaded_file moves the temporary file to the target.
Random filename and storage
The original filename is user controlled; it may contain a path traversal (../../etc/passwd). So you generate a 32-character random name with bin2hex(random_bytes(16)) and append the allowlist extension. The file is stored outside the web root, e.g. /var/www/uploads; downloads go through a controller script.
The allowlist is kept as ['png', 'jpg', 'pdf']; strict check uses in_array($ext, $allowed, true). move_uploaded_file moves the temporary file to the target and blocks path traversal. As output you should print only the name's length or a status code; never print the random name itself.
Exercise Write a generateUploadName function
Define generateUploadName(string $ext, array $allowed): string. Return an empty string when in_array($ext, $allowed, true) is false. If true, return bin2hex(random_bytes(8)) . "." . $ext. Define $allowed = ["png", "jpg", "pdf"]. Call generateUploadName("png", $allowed) and generateUploadName("exe", $allowed); print 1/0 for whether the first is 20 long and the second is empty.
Show example solution
string { if (!in_array($ext, $allowed, true)) { return ""; } return bin2hex(random_bytes(8)) . "." . $ext; }. "png" is valid → bin2hex(random_bytes(8)) = 16 hex + ".png" = 20 characters → strlen 20 = true (1). "exe" is invalid → empty string → === "" true (1).
Example solution:
<?php
function generateUploadName(string $ext, array $allowed): string {
if (!in_array($ext, $allowed, true)) {
return "";
}
return bin2hex(random_bytes(8)) . "." . $ext;
}
$allowed = ["png", "jpg", "pdf"];
$name = generateUploadName("png", $allowed);
$bad = generateUploadName("exe", $allowed);
echo (strlen($name) === 20 ? "1" : "0") . "\n";
echo ($bad === "" ? "1" : "0") . "\n";
Test yourself
-
Match the upload security concepts
- finfo
- Reads real MIME
- random_bytes
- Generates random name
- bin2hex
- Converts to hex string
- allowlist
- Extension check
-
How do you read the real MIME of an uploaded file?
- With $_FILES['file']['type']
- With pathinfo()
- With basename()
- With the finfo class
Show answer
With the finfo class — Correct. finfo reads the MIME from the file's content; the untrusted hint is not relied on.
-
Generate a name with random_bytes
$name = bin2hex(____(16)) . ".png";
Show answer
$name = bin2hex(random_bytes(16)) . ".png";
Next lesson: Summary - Advanced security