Web Form File Validation Implementation Checklist
Implementing file upload functionality involves numerous security considerations and many easily overlooked pitfalls. This article explains a checklist for implementing file uploads that operate safely in production environments.
Checklist Overview
This checklist focuses on backend (server-side) validation. Frontend validation is implemented as a supplementary UX improvement, but it provides no security guarantees.
1. File size validation
- Upload limit is defined in bytes (no confusion between MB and MiB)
- For PHP, both
upload_max_filesizeandpost_max_sizeare configured - For Nginx,
client_max_body_sizeincludes an allowance for multipart overhead - Error handling is in place for when
$_FILES['file']['error']is UPLOAD_ERR_INI_SIZE / UPLOAD_ERR_FORM_SIZE - Minimum file size check is in place (excluding 0-byte files)
$maxBytes) {
throw new \RuntimeException(sprintf(
'ファイルサイズ(%s)が上限(%s)を超えています',
number_format($file['size']),
number_format($maxBytes)
));
}
}
2. File format validation (MIME type)
- The
Content-Type($_FILES['file']['type']) sent from the client is not trusted - Server-side MIME type validation is performed using
finfo/mime_content_type() - A whitelist of allowed MIME types is defined
file($file['tmp_name']);
if (!in_array($mimeType, $allowed, true)) {
throw new \RuntimeException('許可されていないファイル形式です: ' . $mimeType);
}
3. File extension validation
- A whitelist of file extensions is defined (whitelist, not blacklist)
- Double extensions (e.g.,
shell.php.jpg) are detected and rejected - Case normalization is applied during validation (treating
.JPGand.jpgas the same)
2) {
throw new \RuntimeException('不正なファイル名です');
}
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed, true)) {
throw new \RuntimeException('許可されていない拡張子です: ' . $ext);
}
4. Magic byte (file signature) validation
- Magic bytes are validated for critical files (e.g., excluding executable files)
5. Safe Handling of Save Destination and File Name
- The destination directory is outside the web root (or controlled via XSendFile/X-Accel-Redirect)
- Saved file names are randomly generated (e.g., UUID) and not the original file name
- Path traversal (e.g.,
../../../etc/passwd) is eliminated through validation - The destination directory does not have PHP execution permission (PHP processing disabled via
.htaccessor Nginx configuration)
6. Error Handling and Response
- Appropriate HTTP status codes (200/201) are returned on successful upload
- 413 Payload Too Large is returned when size is exceeded
- 422 Unprocessable Entity is returned for invalid file formats
- Error messages do not contain server internal information (paths, versions, etc.)
7. Test Cases
After implementation, run the following test cases to verify the behavior. You can use the test files available in DevLab.
| Test cases | Expected result | Files to use |
|---|---|---|
| File exactly at the limit | Success | Threshold Files |
| File exceeding the limit by 1 byte | 413 error | Threshold Files |
| Empty file with 0 bytes | Validation Error | Manual Creation |
File with spoofed extension (PHP masked as .jpg) | MIME Error | Broken files |
| Corrupted header file | Validation Error | Broken files |
Summary
Implementing secure file uploads requires validation across multiple layers. In particular, be sure to implement the following three points.
- MIME type validation on server side (
finfousage) — Do not trust client declarations - Save with random filename — Do not use the original filename
- Disable PHP execution in the upload directory — Prevent scripts from executing in the upload directory