File Uploads

Course 2 · Ch 8
File Uploads and Handling User Input Safely
Accepting files from a visitor without letting them compromise the server, and closing the output-escaping gap left open since Fundamentals

Chapter 8 of Fundamentals flagged, but deliberately deferred, two things: output escaping and proper input validation. This chapter closes both gaps, and adds file uploads — one of the highest-risk features a web form can offer, since it involves a visitor placing an actual file onto the server's disk.

htmlspecialchars() — Closing the XSS Gap

<?php $comment = $_POST['comment'] ?? ''; // UNSAFE — exactly what Fundamentals Chapter 8 warned about: echo "You said: $comment"; // SAFE — escapes HTML special characters before they're ever rendered: echo "You said: " . htmlspecialchars($comment); ?>

If a visitor submits <script>alert('hacked')</script> as their comment, the unsafe version actually runs that script in every other visitor's browser who views the page — a Cross-Site Scripting (XSS) attack. htmlspecialchars() converts <, >, &, and quotes into their safe HTML entity equivalents, so the text displays literally instead of being interpreted as markup.

Escape on OUTPUT, not on input
A common mistake is running htmlspecialchars() once when data is first received, then storing the escaped version. This causes problems later (double-escaping, or needing the raw value for non-HTML purposes). The standard practice is to store the original input as-is, and escape it specifically at the point it's about to be echoed into HTML — every single time it's displayed.

Validating Input Properly

<?php $email = $_POST['email'] ?? ''; $age = $_POST['age'] ?? ''; if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { echo "Please enter a valid email address.<br>"; } if (!filter_var($age, FILTER_VALIDATE_INT, ["options" => ["min_range" => 0, "max_range" => 130]])) { echo "Please enter a valid age.<br>"; } ?>

filter_var() with PHP's built-in filters handles common validation needs (email format, integer ranges, URLs) far more reliably than hand-written checks — FILTER_VALIDATE_EMAIL correctly handles the genuinely complex rules of what counts as a valid email address, something error-prone to get exactly right manually.

File Uploads — The HTML Side

<form action="upload.php" method="post" enctype="multipart/form-data"> <input type="file" name="avatar"> <input type="submit"> </form>
enctype="multipart/form-data" is easy to forget, and silently breaks file uploads
Without this attribute on the <form> tag, the file's actual contents are never sent to the server at all — $_FILES would simply be empty, with no obvious error to explain why.

$_FILES — The PHP Side

<?php print_r($_FILES['avatar']); // Array // ( // [name] => photo.jpg — the original filename from the visitor's computer // [type] => image/jpeg — claimed type, NOT to be trusted blindly // [tmp_name] => /tmp/phpA1B2C3 — where PHP temporarily stored the uploaded file // [error] => 0 — 0 means success; any other value means a problem // [size] => 204800 — size in bytes // ) ?>

Handling an Upload Safely

<?php $allowedTypes = ['image/jpeg', 'image/png']; $maxSize = 2 * 1024 * 1024; // 2 MB if ($_FILES['avatar']['error'] !== UPLOAD_ERR_OK) { echo "Upload failed.<br>"; } elseif ($_FILES['avatar']['size'] > $maxSize) { echo "File too large (max 2MB).<br>"; } else { $actualType = mime_content_type($_FILES['avatar']['tmp_name']); // checks the REAL file content, not the claimed type if (!in_array($actualType, $allowedTypes)) { echo "Invalid file type.<br>"; } else { $newName = uniqid() . '.jpg'; // NEVER trust the original filename directly move_uploaded_file($_FILES['avatar']['tmp_name'], "uploads/$newName"); echo "Upload successful.<br>"; } } ?>
$_FILES['avatar']['type'] is claimed by the browser — it can be faked entirely
A malicious visitor can rename a script file to photo.jpg and have the browser report image/jpeg as the type, despite the actual content being something else entirely (like executable PHP code). mime_content_type() inspects the file's real content rather than trusting the claim — checking the genuine type is essential before deciding whether to accept an upload.
Never use the visitor's original filename directly when saving the file
A filename like ../../config.php could, if used unmodified, attempt to overwrite a completely different file outside the intended uploads folder (a "path traversal" attack). Generating a new, safe filename (as with uniqid() above) and discarding the original name entirely avoids this risk altogether.
FunctionWhat it does
htmlspecialchars($s)Escapes HTML special characters before output — prevents XSS
filter_var($val, FILTER)Validates a value against a built-in rule (email, int range, URL)
$_FILES['name']Information about an uploaded file (name, type, tmp_name, error, size)
mime_content_type($path)Checks the REAL type of a file's content, not the claimed type
move_uploaded_file($tmp, $dest)Moves an upload from its temporary location to its final destination

Coding Challenges

Challenge 1

Write a script that takes a $_POST['feedback'] value containing HTML special characters (test with a string like "<b>Great site!</b>"), and echoes it twice — once unescaped (commented out as unsafe), and once safely with htmlspecialchars(). Explain in a comment what would happen if the unsafe version were used with a real <script> tag instead.

📄 View solution
Challenge 2

Write a function validateRegistration($email, $age) that uses filter_var() to check both, returning an array of error messages (empty if everything is valid). Test it with one fully valid set of inputs and one fully invalid set.

📄 View solution
Challenge 3

Write a complete safe file-upload handler for a field named "document" that only allows PDF files (application/pdf) up to 5MB, checks the real MIME type with mime_content_type(), generates a new filename with uniqid(), and moves the file into an "uploads/" folder — echoing clear success or failure messages at each validation step.

📄 View solution

Chapter 8 Quick Reference

  • htmlspecialchars($s) — escape on OUTPUT, every time, to prevent XSS
  • filter_var($val, FILTER_VALIDATE_*) — reliable built-in validation (email, int range, URL)
  • enctype="multipart/form-data" — required on the form tag for file uploads to work at all
  • $_FILES['field'] — name, type (untrusted!), tmp_name, error, size
  • mime_content_type($tmp_name) — checks the file's REAL type, never trust the claimed type
  • Never use the original filename directly — generate a new one to avoid path traversal
  • move_uploaded_file($tmp, $dest) — finalises the upload to its destination folder
  • Next chapter: regular expressions in PHP — preg_match, preg_replace