Skip to content

Web Form File Validation Implementation Checklist

Category: Security / Implementation
This article is currently available in Japanese only. We are working on translations.

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.

Recommended file validation order Safe order: light to heavy checks 1. File size bytes <= max NG → reject immediately 2. Extension .jpg / .png ... NG → reject immediately 3. MIME type finfo / mime_content_type NG → reject immediately 4. Magic bytes FF D8 FF (JPEG) ... NG → reject immediately 5. Content scan ClamAV / VirusTotal NG → reject immediately
Fig 1: Run lightweight validations first and reject early

Checklist Overview

This checklist focuses on <strong>backend (server-side) validation</strong>. Frontend validation is implemented as a supplementary UX improvement, but it provides no security guarantees.

1. File size validation

  • <input type="checkbox" disabled> Upload limit is defined in bytes (no confusion between MB and MiB)
  • <input type="checkbox" disabled> For PHP, both <code>upload_max_filesize</code> and <code>post_max_size</code> are configured
  • <input type="checkbox" disabled> For Nginx, <code>client_max_body_size</code> includes an allowance for multipart overhead
  • <input type="checkbox" disabled> Error handling is in place for when <code>$_FILES['file']['error']</code> is UPLOAD_ERR_INI_SIZE / UPLOAD_ERR_FORM_SIZE
  • <input type="checkbox" disabled> 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)

  • <input type="checkbox" disabled> The <code>Content-Type</code> (<code>$_FILES['file']['type']</code>) sent from the client is not trusted
  • <input type="checkbox" disabled> Server-side MIME type validation is performed using <code>finfo</code> / <code>mime_content_type()</code>
  • <input type="checkbox" disabled> 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

  • <input type="checkbox" disabled> A whitelist of file extensions is defined (whitelist, not blacklist)
  • <input type="checkbox" disabled> Double extensions (e.g., <code>shell.php.jpg</code>) are detected and rejected
  • <input type="checkbox" disabled> Case normalization is applied during validation (treating <code>.JPG</code> and <code>.jpg</code> as 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

  • <input type="checkbox" disabled> Magic bytes are validated for critical files (e.g., excluding executable files)

5. Safe Handling of Save Destination and File Name

  • <input type="checkbox" disabled> The destination directory is outside the web root (or controlled via XSendFile/X-Accel-Redirect)
  • <input type="checkbox" disabled> Saved file names are randomly generated (e.g., UUID) and not the original file name
  • <input type="checkbox" disabled> Path traversal (e.g., <code>../../../etc/passwd</code>) is eliminated through validation
  • <input type="checkbox" disabled> The destination directory does not have PHP execution permission (PHP processing disabled via <code>.htaccess</code> or Nginx configuration)

6. Error Handling and Response

  • <input type="checkbox" disabled> Appropriate HTTP status codes (200/201) are returned on successful upload
  • <input type="checkbox" disabled> <strong>413 Payload Too Large</strong> is returned when size is exceeded
  • <input type="checkbox" disabled> <strong>422 Unprocessable Entity</strong> is returned for invalid file formats
  • <input type="checkbox" disabled> 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 casesExpected resultFiles to use
File exactly at the limitSuccess<a href="/ja/files/threshold/">Threshold Files</a>
File exceeding the limit by 1 byte413 error<a href="/ja/files/threshold/">Threshold Files</a>
Empty file with 0 bytesValidation ErrorManual Creation
File with spoofed extension (PHP masked as <code>.jpg</code>)MIME Error<a href="/ja/files/broken/">Broken files</a>
Corrupted header fileValidation Error<a href="/ja/files/broken/">Broken files</a>

Summary

Implementing secure file uploads requires validation across multiple layers. In particular, be sure to implement the following three points.

  1. <strong>MIME type validation on server side</strong> (<code>finfo</code> usage) — Do not trust client declarations
  2. <strong>Save with random filename</strong> — Do not use the original filename
  3. <strong>Disable PHP execution in the upload directory</strong> — Prevent scripts from executing in the upload directory

Test files for this article

  • → <a href="/ja/files/broken/" class="text-primary-600 dark:text-primary-400 hover:underline">Broken Files List (For Testing Validation Errors)</a>
  • → <a href="/ja/files/threshold/" class="text-primary-600 dark:text-primary-400 hover:underline">Threshold Test Files (9.9MB / 10MB / 10.1MB)</a>
  • → <a href="/ja/files/images/" class="text-primary-600 dark:text-primary-400 hover:underline">Image Test Files List (PNG / JPG / WebP / GIF)</a>

Related articles

  • → <a href="/ja/blog/how-to-test-upload-limit/" class="text-primary-600 dark:text-primary-400 hover:underline">How to Properly Test File Upload Limits</a>
  • → <a href="/ja/reference/magic-bytes/" class="text-primary-600 dark:text-primary-400 hover:underline">Magic bytes (file signature) reference</a>