Накратко: Когато браузърът качва файл, той изпраща POST заявка във формат multipart/form-data, в която полетата на формата и съдържанието на файловете стоят като отделни части една след друга. PHP сам разчленява тази заявка, записва всяка файлова част във временен файл в upload_tmp_dir, вписва данните в $_FILES и едва тогава стартира Вашия скрипт. Скриптът премества файла с move_uploaded_file(); всичко, което не е преместено, PHP изтрива в края на заявката.
Пътят на един файл от браузъра до скрипта
За вземащите решения най-важното е следното: качването не е процес само на приложението. Голяма част от работата се извършва в интерпретатора на PHP, преди кодът на CMS или на магазина изобщо да участва. Затова грешки в тази част на PHP засягат всеки уебсайт, независимо дали има форма за качване.
Последователността накратко:
- Формата с
method="POST"иenctype="multipart/form-data"се изпраща. - Уеб сървърът приема заявката и я предава на PHP, според режима на работа през PHP-FPM, mod_php или друг интерфейс.
- PHP проверява ограничения като
post_max_sizeи разчленява съдържанието по разделителна последователност, т.нар. boundary. - Обикновените полета попадат в
$_POST, файловите части се записват във временни файлове и се описват в$_FILES. - Едва сега скриптът стартира, проверява файла и го премества на целевото място.
- В края на заявката PHP изтрива всички временни файлове, които не са преместени.
multipart/form-data: структурата на една заявка за качване
Заявката за качване е чист текст с вградени файлови данни. Хедърът Content-Type назовава boundary, а в съдържанието тази последователност от знаци разделя отделните части. Всяка част има собствени заглавни редове с име на полето и евентуално име на файла. Силно съкратен, безобиден пример:
POST /bewerbung HTTP/1.1
Content-Type: multipart/form-data; boundary=----trenner42
------trenner42
Content-Disposition: form-data; name="vorname"
Erika
------trenner42
Content-Disposition: form-data; name="lebenslauf"; filename="lebenslauf.pdf"
Content-Type: application/pdf
%PDF-1.7 ... Dateiinhalt ...
------trenner42--
Всички данни в този текст идват от подателя, включително името на файла и Content-Type на частта. PHP ги вписва в $_FILES, но не проверява достоверността им.
Масивът $_FILES
За поле с име lebenslauf $_FILES['lebenslauf'] съдържа според ръководството на PHP следните ключове:
| Ключ | Съдържание | Заслужава ли доверие? |
|---|---|---|
name |
първоначалното име на файла при подателя | не |
full_path |
път, съобщен от браузъра, от PHP 8.1.0 | не |
type |
MIME тип според браузъра, непроверен от PHP | не |
size |
размер в байтове | определя се от PHP |
tmp_name |
път на временния файл на сървъра | задава се от PHP |
error |
код за грешка при обработката на качването | задава се от PHP |
При полета с квадратни скоби в името, например bilder[], всеки ключ съдържа масив с по един запис за файл. Приложението тогава обхожда например $_FILES['bilder']['error'] и през същия индекс достъпва tmp_name и name.
Една форма може допълнително да изпрати скрито поле MAX_FILE_SIZE пред полето за файл. Според ръководството това е само подсказка за браузъра, която лесно се заобикаля. Можете да разчитате единствено на ограниченията, които сървърът налага: в PHP, в уеб сървъра и в самото приложение.
Ограничения: директивите на php.ini за качване
Тези директиви управляват обработката на качването. Стойностите по подразбиране са от ръководството на PHP и важат, когато никой php.ini не ги презаписва.
| Директива | По подразбиране | Променяема в |
|---|---|---|
file_uploads |
1 |
само системна конфигурация |
upload_tmp_dir |
празно (временна директория на системата) | само системна конфигурация |
upload_max_filesize |
2M |
php.ini, пул, конфигурация по директории |
post_max_size |
8M |
php.ini, пул, конфигурация по директории |
max_file_uploads |
20 |
php.ini, пул, конфигурация по директории |
enable_post_data_reading |
1 |
php.ini, пул, конфигурация по директории |
post_max_size трябва да е по-голям от upload_max_filesize, а memory_limit според ръководството по правило трябва да е по-голям от post_max_size. Ако upload_tmp_dir не е достъпна за запис, PHP преминава към временната директория на системата. При open_basedir временната директория трябва да е разрешена, иначе качването се проваля. Практически стойности и техните последствия описва статията Разумно ограничаване на post_max_size и upload_max_filesize.
Правилно оценяване на кодовете за грешки
Полето error показва какво е станало при обработката. Константите са от UPLOAD_ERR_OK (0) през UPLOAD_ERR_INI_SIZE (1), UPLOAD_ERR_FORM_SIZE (2), UPLOAD_ERR_PARTIAL (3) и UPLOAD_ERR_NO_FILE (4) до UPLOAD_ERR_NO_TMP_DIR (6), UPLOAD_ERR_CANT_WRITE (7) и UPLOAD_ERR_EXTENSION (8).
<?php
declare(strict_types=1);
$datei = $_FILES['lebenslauf'] ?? null;
if ($datei === null) {
exit('Keine Upload-Daten, eventuell wurde post_max_size überschritten.');
}
$meldung = match ($datei['error']) {
UPLOAD_ERR_OK => null,
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'Datei zu groß.',
UPLOAD_ERR_NO_FILE => 'Keine Datei gewählt.',
default => 'Upload fehlgeschlagen.',
};
if ($meldung === null && is_uploaded_file($datei['tmp_name'])) {
$ziel = '/srv/app/uploads/' . bin2hex(random_bytes(16)) . '.pdf';
move_uploaded_file($datei['tmp_name'], $ziel);
}
Примерът показва само механиката. Проверката на типа на файла, съхранението извън webroot и блокирането на изпълнението на скриптове описва наръчникът Сигурно реализиране на качването на файлове в PHP.
Качване и PHP2Shell: какво означават временните мерки
Уязвимостите PHP2Shell CVE-2026-109618 и CVE-2026-109619 засягат обработката около заявката в интерпретатора на PHP. Затова php.net посочва като временни мерки, наред с другото, post_max_size = 8M (без качване) или 32M (с качване), upload_max_filesize под тази стойност, както и session.upload_progress.enabled=0. Тези стойности принадлежат в php.ini, FPM пула или конфигурацията на сървъра, след което всички worker процеси трябва да се рестартират.
Специализирани автори предлагат допълнително да се блокират заявките multipart/form-data извън истинските пътища за качване на прокси или в WAF. Това е неофициална мярка с риск формите да спрат да работят; преценката описва статията Филтриране на заявките за качване на периферията. Нито една от тези мерки не замества актуализацията до PHP 8.5.12, 8.4.27, 8.3.36 или 8.2.35.
Как помага PHP SOS конкретно?
- Установяваме кои от Вашите уебсайтове действително се нуждаят от качване и по кои пътища.
- Задаваме ограниченията и временните мерки за всеки пул, рестартираме всички worker процеси и тестваме формите.
- Проверяваме директориите за качване за изпълними файлове и блокираме изпълнението на скриптове в рамките на заздравяването на сървъра.
- Внедряваме актуализацията на PHP и документираме състоянието на пачовете за всеки уебсайт.
Често задавани въпроси
Къде PHP съхранява първоначално качените файлове?
Във временна директория. По подразбиране това е временната директория на системата; с upload_tmp_dir тя може да се промени. Ако файлът не бъде преместен в скрипта, PHP го изтрива в края на заявката.
Мога ли да вярвам на полето type в $_FILES?
Не. MIME типът се съобщава от браузъра, а PHP според ръководството не го проверява. И name, и full_path идват от подателя. Действителния тип на файла определяте от страна на сървъра по съдържанието.
Защо $_FILES е празен, макар да е изпратен файл?
Най-често цялата заявка е била по-голяма от post_max_size. Тогава PHP изчиства напълно $_POST и $_FILES, без код за грешка в масива. Други причини са липсващ enctype във формата или изключена настройка file_uploads.
Помага ли срещу PHP2Shell, ако забраня качването в приложението?
Не. Обработката на данните от заявката се извършва в PHP, преди да се изпълни кодът на приложението. Ефективни са актуализацията на PHP и рестартирането на всички worker процеси; по-тесните ограничения са само временна мярка.
Каква е разликата между upload_max_filesize и post_max_size?
upload_max_filesize ограничава всеки отделен файл, а post_max_size – цялото съдържание на заявката, включително всички файлове и полета. Затова post_max_size трябва да е по-голям от upload_max_filesize.