So implementieren Sie Datei-Uploads in PHP|Vollständiges Leitfaden für Validierung, Speicherung und Sicherheit
Bei der Implementierung von Datei-Upload-Funktionen in PHP ist es einfach, Code zu schreiben, der 「einfach funktioniert」, aber eine robuste Implementierung unter Berücksichtigung von Sicherheit, Validierung und Fehlerbehandlung ist überraschend komplex. Dieser Artikel erklärt systematisch von der Struktur von <code>$_FILES</code> über die Validierung von MIME-Typen, Extension-Whitelists, sichere Speicherorte und Desinfizierung von Dateinamen bis hin zu Implementierungsmethoden auf Produktionsebene.
Die Struktur von <code>$_FILES</code> verstehen
Beim Senden von Dateien in einem HTML-Formular mit <code>enctype="multipart/form-data"</code> speichert PHP die Upload-Informationen in der Superglobalen <code>$_FILES</code>. Wenn das Attribut <code>name</code> des Formulars <code>userfile</code> ist, wird <code>$_FILES['userfile']</code> ein assoziatives Array mit den folgenden 5 Schlüsseln.
// $_FILES の構造
[
'name' => 'photo.jpg', // クライアント側のファイル名
'type' => 'image/jpeg', // クライアントが申告するMIMEタイプ
'tmp_name' => '/tmp/phpA1B2C3', // サーバーの一時保存パス
'error' => 0, // エラーコード(0 = 成功)
'size' => 204800, // ファイルサイズ(バイト)
]
Als wichtiger Punkt ist <code>type</code> ein vom Client gesendeter Wert und kann gefälscht werden. Überprüfen Sie für die MIME-Typ-Validierung unbedingt auf der Serverseite mit <code>finfo</code>.
Liste der Fehlercodes UPLOAD_ERR_* und Lösungen
<code>$_FILES['userfile']['error']</code> enthält einen der folgenden Konstantenwerte. Es ist wichtig, diesen Wert zu Beginn des Upload-Prozesses zu überprüfen und Fehler angemessen zu behandeln.
| Konstante | Wert | Bedeutung | Lösungsmethode |
|---|---|---|---|
UPLOAD_ERR_OK |
0 | Upload erfolgreich | Verarbeitung fortsetzen |
UPLOAD_ERR_INI_SIZE |
1 | Limit von upload_max_filesize in php.ini überschritten | Überprüfung der php.ini-Konfiguration |
UPLOAD_ERR_FORM_SIZE |
2 | MAX_FILE_SIZE-Limit des HTML-Formulars überschritten | Formulareinstellungen überprüfen |
UPLOAD_ERR_PARTIAL |
3 | Datei nur teilweise übertragen | Neu-Upload fördern |
UPLOAD_ERR_NO_FILE |
4 | Keine Datei ausgewählt | Zur Dateauswahl auffordern |
UPLOAD_ERR_NO_TMP_DIR |
6 | Temporäres Verzeichnis existiert nicht | Servereinstellungen überprüfen |
UPLOAD_ERR_CANT_WRITE |
7 | Fehler beim Schreiben auf den Datenträger | Datenträgerkapazität und Berechtigungen überprüfen |
UPLOAD_ERR_EXTENSION |
8 | Upload durch PHP-Erweiterung gestoppt | Konfiguration des Erweiterungsmoduls überprüfen |
function getUploadErrorMessage(int $errorCode): string
{
return match ($errorCode) {
UPLOAD_ERR_INI_SIZE => 'ファイルサイズがサーバーの上限を超えています。',
UPLOAD_ERR_FORM_SIZE => 'ファイルサイズがフォームの上限を超えています。',
UPLOAD_ERR_PARTIAL => 'ファイルが完全にアップロードされませんでした。再度お試しください。',
UPLOAD_ERR_NO_FILE => 'ファイルが選択されていません。',
UPLOAD_ERR_NO_TMP_DIR => 'サーバーエラー: 一時ディレクトリが見つかりません。',
UPLOAD_ERR_CANT_WRITE => 'サーバーエラー: ファイルの書き込みに失敗しました。',
UPLOAD_ERR_EXTENSION => 'サーバーエラー: 拡張機能によりアップロードが拒否されました。',
default => '不明なエラーが発生しました。',
};
}
MIME-Typ-Validierung (mit finfo)
<code>$_FILES['userfile']['type']</code>, das vom Client gesendet wird, kann gefälscht werden. Daher ist es notwendig, den MIME-Typ aus dem tatsächlichen Dateiinhalt auf der Serverseite zu bestimmen. Verwenden Sie <code>finfo_file()</code> von PHP (oder die Klasse <code>FileInfo</code>).
function validateMimeType(string $tmpPath, array $allowedMimes): bool
{
$finfo = new finfo(FILEINFO_MIME_TYPE);
$detectedMime = $finfo->file($tmpPath);
return in_array($detectedMime, $allowedMimes, true);
}
// 使用例: 画像ファイルのみ許可
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!validateMimeType($_FILES['userfile']['tmp_name'], $allowedMimes)) {
throw new RuntimeException('許可されていないファイル形式です。');
}
<code>finfo</code> liest die ersten Bytes der Datei (Magic Number), um den MIME-Typ zu bestimmen und kann so auch Angriffe mit gefälschten Erweiterungen abwehren. Textdateien (CSV, JSON usw.) können jedoch als <code>text/plain</code> bestimmt werden, daher wird empfohlen, diese mit einer Erweiterungsprüfung zu kombinieren.
Dateityp-Whitelist
Implementieren Sie zusammen mit der MIME-Typ-Validierung auch eine Whitelist-Überprüfung von Dateierweiterungen. Eine Whitelist (nur erlaubte Erweiterungen auflisten) ist sicherer als eine Blacklist (.php, .exe usw. verbieten).
function validateExtension(string $originalName, array $allowedExtensions): bool
{
// pathinfo() で拡張子を取得し、小文字に正規化
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
return in_array($extension, $allowedExtensions, true);
}
// 使用例
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!validateExtension($_FILES['userfile']['name'], $allowedExtensions)) {
throw new RuntimeException('許可されていない拡張子です。');
}
// MIMEタイプと拡張子の対応を明示的にマッピングする方法
$mimeToExtensions = [
'image/jpeg' => ['jpg', 'jpeg'],
'image/png' => ['png'],
'image/gif' => ['gif'],
'image/webp' => ['webp'],
];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$detectedMime = $finfo->file($_FILES['userfile']['tmp_name']);
$extension = strtolower(pathinfo($_FILES['userfile']['name'], PATHINFO_EXTENSION));
if (
!isset($mimeToExtensions[$detectedMime]) ||
!in_array($extension, $mimeToExtensions[$detectedMime], true)
) {
throw new RuntimeException('ファイル形式と拡張子が一致しません。');
}
Codebeispiel für Dateigröße-Limit
Die Größenprüfung auf PHP-Ebene wird mit <code>$_FILES['userfile']['size']</code> durchgeführt. Dateien, die jedoch <code>upload_max_filesize</code> in php.ini überschreiten, erreichen <code>$_FILES</code> nicht und führen zu einem <code>UPLOAD_ERR_INI_SIZE</code>-Fehler. Daher ist es wichtig, zunächst den Fehlercode zu überprüfen.
const MAX_UPLOAD_SIZE = 10 * 1024 * 1024; // 10 MiB
function validateFileSize(int $fileSize, int $maxSize = MAX_UPLOAD_SIZE): bool
{
return $fileSize > 0 && $fileSize <= $maxSize;
}
// 使用例
if (!validateFileSize($_FILES['userfile']['size'])) {
$maxMiB = MAX_UPLOAD_SIZE / (1024 * 1024);
throw new RuntimeException("ファイルサイズが上限({$maxMiB} MiB)を超えています。");
}
// アップロード処理を関数にまとめた例
function processUpload(array $file): array
{
// 1. エラーコード確認
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException(getUploadErrorMessage($file['error']));
}
// 2. サイズチェック
if (!validateFileSize($file['size'])) {
throw new RuntimeException('ファイルサイズが上限を超えています。');
}
// 3. MIMEタイプチェック
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!validateMimeType($file['tmp_name'], $allowedMimes)) {
throw new RuntimeException('許可されていないファイル形式です。');
}
// 4. 拡張子チェック
$allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!validateExtension($file['name'], $allowedExts)) {
throw new RuntimeException('許可されていない拡張子です。');
}
return $file;
}
Sicherer Speicherort (außerhalb von public)
Hochgeladene Dateien sollten außerhalb des Dokumentstammverzeichnisses (public_html / public usw.) gespeichert werden. Wenn sie im public-Verzeichnis gespeichert werden, besteht die Gefahr des direkten Zugriffs über URLs oder der Ausführung als PHP-Dateien.
// 推奨ディレクトリ構成
// /var/www/
// ├── public/ ← ドキュメントルート(外部からアクセス可能)
// │ └── index.php
// └── storage/ ← public の外(外部から直接アクセス不可)
// └── uploads/
// 定数で保存先を明示
define('UPLOAD_DIR', dirname(__DIR__) . '/storage/uploads/');
// ディレクトリが存在しない場合は作成
if (!is_dir(UPLOAD_DIR)) {
mkdir(UPLOAD_DIR, 0755, true);
}
// ダウンロード提供時は PHP 経由でストリーミング
function serveFile(string $filename): void
{
$filePath = UPLOAD_DIR . basename($filename);
if (!file_exists($filePath)) {
http_response_code(404);
exit;
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($filePath);
header('Content-Type: ' . $mimeType);
header('Content-Length: ' . filesize($filePath));
header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
readfile($filePath);
exit;
}
ファイル名のサニタイズ(uniqid使用)
クライアントが送信したファイル名をそのまま使用するのは危険です。ディレクトリトラバーサル(../../etc/passwd のようなパス)や、特殊文字を含むファイル名によるOSコマンドインジェクションのリスクがあります。uniqid() を使ってユニークなファイル名を生成し、元のファイル名は別途データベースなどに記録する方法が安全です。
function generateSafeFilename(string $originalName): string
{
// 拡張子のみ元のファイルから引き継ぐ
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
// uniqid() + mt_rand() でユニークな名前を生成
// more_entropy=true で精度を上げる
$uniqueName = uniqid('upload_', true);
// さらにランダム性を加える場合は bin2hex(random_bytes(8)) を使う
// $uniqueName = bin2hex(random_bytes(16));
return $uniqueName . '.' . $extension;
}
// 使用例
$safeFilename = generateSafeFilename($_FILES['userfile']['name']);
$savePath = UPLOAD_DIR . $safeFilename;
// is_uploaded_file() で正規のアップロードか確認してから移動
if (!is_uploaded_file($_FILES['userfile']['tmp_name'])) {
throw new RuntimeException('不正なファイルアップロードを検出しました。');
}
if (!move_uploaded_file($_FILES['userfile']['tmp_name'], $savePath)) {
throw new RuntimeException('ファイルの保存に失敗しました。');
}
// 元のファイル名、保存名、MIMEタイプをDBに記録
// db_insert(['original_name' => $_FILES['userfile']['name'], 'saved_name' => $safeFilename, ...]);
php.ini 設定(upload_max_filesize / post_max_size)
Um Datei-Uploads in PHP zu akzeptieren, müssen Sie php.ini (oder lokale Einstellungen in .htaccess / php.ini) ordnungsgemäß konfigurieren. Das Wichtige ist nicht nur <code>upload_max_filesize</code>, sondern auch <code>post_max_size</code> entsprechend zu ändern.
; php.ini の設定
; ファイルアップロードを有効化
file_uploads = On
; 1ファイルあたりの上限(M = MiB 単位)
upload_max_filesize = 20M
; POSTリクエスト全体の上限
; upload_max_filesize より大きく設定する(フォームデータのオーバーヘッド分)
post_max_size = 25M
; 最大実行時間(大きなファイルのアップロードに対応)
max_execution_time = 300
; 最大入力時間(アップロードの読み込み時間)
max_input_time = 300
; メモリ上限(post_max_size より大きくする)
memory_limit = 128M
Sie können überprüfen, ob der Konfigurationswert angewendet wurde, indem Sie <code>phpinfo()</code> oder <code>ini_get()</code> verwenden.
// 現在の設定値を確認
echo ini_get('upload_max_filesize'); // 例: "20M"
echo ini_get('post_max_size'); // 例: "25M"
// バイト単位に変換するユーティリティ
function convertToBytes(string $value): int
{
$value = trim($value);
$last = strtolower($value[-1]);
$num = (int) $value;
return match ($last) {
'g' => $num * 1024 * 1024 * 1024,
'm' => $num * 1024 * 1024,
'k' => $num * 1024,
default => $num,
};
}
Zusammenfassung: Checkliste für sicheres Upload-Handling
- Überprüfung von Fehlercodes mit UPLOAD_ERR_*
- MIME-Typ wird auf der Serverseite mit <code>finfo</code> bestimmt
- Validierung der Dateierweiterung mit Whitelist-Methode
- Dateigröße mit <code>$_FILES['userfile']['size']</code> überprüfen
- Das Speicherziel ist außerhalb des Verzeichnisses public konfiguriert
- Überprüfung von legitimen Uploads mit <code>is_uploaded_file()</code>
- Datei wird mit <code>move_uploaded_file()</code> verschoben
- Generierung von Dateinamen mit <code>uniqid()</code> oder <code>random_bytes()</code>
- <code>upload_max_filesize</code> und <code>post_max_size</code> in php.ini ordnungsgemäß konfiguriert
Testdatei zur Verwendung in diesem Artikel
- <a href="/ja/files/threshold/" class="text-primary-600 dark:text-primary-400 hover:underline">Grenzwert-Testdateiliste</a> — Ideal zur Überprüfung der Werte um die <code>upload_max_filesize</code>-Einstellung
- <a href="/ja/files/threshold/10mb/" class="text-primary-600 dark:text-primary-400 hover:underline">10 MB Grenzwert-Testsatz</a> — 3 Dateien: genau am Limit, davor und danach
- <a href="/ja/files/broken/" class="text-primary-600 dark:text-primary-400 hover:underline">Liste beschädigter und ungültiger Dateien</a> — Testen Sie die Validierung mit gefälschtem MIME-Typ und gefälschter Dateierweiterung
- <a href="/ja/files/images/png/" class="text-primary-600 dark:text-primary-400 hover:underline">PNG-Testbilderliste</a> — Zur MIME-Typ-Validierung beim Bild-Upload
Verwandte Artikel
- <a href="/ja/blog/how-to-test-upload-limit/" class="text-primary-600 dark:text-primary-400 hover:underline">So testen Sie die Datei-Upload-Grenze korrekt</a>
- <a href="/ja/blog/file-validation-checklist/" class="text-primary-600 dark:text-primary-400 hover:underline">Checkliste zur Implementierung der Dateivalidierung in Webformularen</a>
- <a href="/ja/blog/multipart-form-data-overhead/" class="text-primary-600 dark:text-primary-400 hover:underline">Den Overhead von multipart/form-data genau berechnen</a>
- <a href="/ja/blog/s3-upload-limit/" class="text-primary-600 dark:text-primary-400 hover:underline">Zusammenfassung der Datei-Upload-Limits für AWS S3 und CloudFront</a>