Krótko: Gdy przeglądarka przesyła plik, wysyła żądanie POST w formacie multipart/form-data, w którym pola formularza i zawartość plików występują jako kolejne osobne części. PHP samo rozbija to żądanie, zapisuje każdą część z plikiem w pliku tymczasowym w upload_tmp_dir, wpisuje dane do $_FILES i dopiero wtedy uruchamia Państwa skrypt. Skrypt przenosi plik za pomocą move_uploaded_file(); wszystko, czego nie przeniesie, PHP usuwa na końcu żądania.
Droga pliku od przeglądarki do skryptu
Dla osób podejmujących decyzje ważne jest przede wszystkim jedno: przesyłanie plików nie jest operacją samej aplikacji. Duża część pracy odbywa się w interpreterze PHP, zanim kod CMS lub sklepu w ogóle weźmie w niej udział. Dlatego błędy w tej części PHP dotyczą każdej strony, niezależnie od tego, czy ma formularz przesyłania plików.
Przebieg w skrócie:
- Formularz z
method="POST"ienctype="multipart/form-data"zostaje wysłany. - Serwer WWW przyjmuje żądanie i przekazuje je do PHP, zależnie od trybu pracy przez PHP-FPM, mod_php lub inny interfejs.
- PHP sprawdza limity, takie jak
post_max_size, i rozbija zawartość na podstawie ciągu znaków rozdzielającego, czyli tzw. boundary. - Zwykłe pola trafiają do
$_POST, części z plikami są zapisywane w plikach tymczasowych i opisywane w$_FILES. - Dopiero teraz zaczyna działać skrypt, sprawdza plik i przenosi go do miejsca docelowego.
- Na końcu żądania PHP usuwa wszystkie pliki tymczasowe, które nie zostały przeniesione.
multipart/form-data: budowa żądania z plikiem
Żądanie z plikiem to zwykły tekst z osadzonymi danymi pliku. Nagłówek Content-Type podaje boundary, a w treści ten ciąg znaków oddziela poszczególne części. Każda część ma własne nagłówki z nazwą pola i ewentualnie nazwą pliku. Mocno skrócony, nieszkodliwy przykład:
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--
Wszystkie dane w tym tekście pochodzą od nadawcy, także nazwa pliku i Content-Type danej części. PHP przejmuje je do $_FILES, ale nie sprawdza ich wiarygodności.
Tablica $_FILES
Dla pola o nazwie lebenslauf tablica $_FILES['lebenslauf'] zawiera według podręcznika PHP następujące klucze:
| Klucz | Zawartość | Wiarygodne? |
|---|---|---|
name |
pierwotna nazwa pliku u nadawcy | nie |
full_path |
ścieżka zgłoszona przez przeglądarkę, od PHP 8.1.0 | nie |
type |
typ MIME według przeglądarki, niesprawdzany przez PHP | nie |
size |
rozmiar w bajtach | ustala PHP |
tmp_name |
ścieżka pliku tymczasowego na serwerze | nadaje PHP |
error |
kod błędu przetwarzania przesyłania | ustawia PHP |
W polach z nawiasami kwadratowymi w nazwie, na przykład bilder[], każdy klucz zawiera tablicę z jednym wpisem na plik. Aplikacja przechodzi wtedy na przykład przez $_FILES['bilder']['error'] i przez ten sam indeks sięga po tmp_name i name.
Formularz może dodatkowo wysłać przed polem pliku ukryte pole MAX_FILE_SIZE. Według podręcznika jest to tylko wskazówka dla przeglądarki, którą łatwo obejść. Polegać można wyłącznie na granicach, które wymusza serwer: w PHP, w serwerze WWW i w samej aplikacji.
Limity: dyrektywy php.ini dla przesyłania plików
Te dyrektywy sterują przetwarzaniem przesyłanych plików. Wartości domyślne pochodzą z podręcznika PHP i obowiązują, gdy żadne php.ini ich nie nadpisuje.
| Dyrektywa | Domyślnie | Zmienialna w |
|---|---|---|
file_uploads |
1 |
tylko konfiguracja systemowa |
upload_tmp_dir |
puste (katalog tymczasowy systemu) | tylko konfiguracja systemowa |
upload_max_filesize |
2M |
php.ini, pula, konfiguracja katalogu |
post_max_size |
8M |
php.ini, pula, konfiguracja katalogu |
max_file_uploads |
20 |
php.ini, pula, konfiguracja katalogu |
enable_post_data_reading |
1 |
php.ini, pula, konfiguracja katalogu |
post_max_size musi być większe niż upload_max_filesize, a memory_limit powinno według podręcznika z reguły być większe niż post_max_size. Jeśli do upload_tmp_dir nie można zapisywać, PHP przechodzi na katalog tymczasowy systemu. Przy open_basedir katalog tymczasowy musi być dozwolony, w przeciwnym razie przesyłanie plików się nie powiedzie. Wartości praktyczne i ich skutki opisuje artykuł Sensowne ograniczanie post_max_size i upload_max_filesize.
Prawidłowa ocena kodów błędów
Pole error mówi, co się stało podczas przetwarzania. Stałe obejmują zakres od UPLOAD_ERR_OK (0) przez UPLOAD_ERR_INI_SIZE (1), UPLOAD_ERR_FORM_SIZE (2), UPLOAD_ERR_PARTIAL (3) i UPLOAD_ERR_NO_FILE (4) do UPLOAD_ERR_NO_TMP_DIR (6), UPLOAD_ERR_CANT_WRITE (7) i 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);
}
Przykład pokazuje tylko mechanikę. Sprawdzanie typu pliku, zapis poza webrootem i blokowanie wykonywania skryptów opisuje poradnik Bezpieczne przesyłanie plików w PHP.
Przesyłanie plików a PHP2Shell: co oznaczają środki przejściowe
Luki PHP2Shell CVE-2026-109618 i CVE-2026-109619 dotyczą przetwarzania w interpreterze PHP związanego z żądaniem. Dlatego php.net podaje jako środki przejściowe między innymi post_max_size = 8M (bez przesyłania plików) lub 32M (z przesyłaniem plików), upload_max_filesize poniżej tej wartości oraz session.upload_progress.enabled=0. Te wartości należą do php.ini, puli FPM lub konfiguracji serwera, po czym wszystkie workery muszą się zrestartować.
Autorzy branżowi proponują dodatkowo blokowanie żądań multipart/form-data poza rzeczywistymi ścieżkami przesyłania plików na proxy lub w WAF. Jest to środek nieoficjalny, z ryzykiem, że formularze przestaną działać; rozważenie za i przeciw opisuje artykuł Filtrowanie żądań przesyłania plików na brzegu. Żaden z tych środków nie zastępuje aktualizacji do PHP 8.5.12, 8.4.27, 8.3.36 lub 8.2.35.
Jak konkretnie pomaga PHP SOS?
- Ustalamy, które z Państwa stron rzeczywiście potrzebują przesyłania plików i na jakich ścieżkach.
- Ustawiamy limity i środki przejściowe dla każdej puli, restartujemy wszystkie workery i testujemy formularze.
- Sprawdzamy katalogi z przesłanymi plikami pod kątem plików wykonywalnych i blokujemy wykonywanie skryptów w ramach utwardzania serwera.
- Instalujemy aktualizację PHP i dokumentujemy stan poprawek dla każdej strony.
Częste pytania
Gdzie PHP zapisuje przesłane pliki w pierwszej kolejności?
W katalogu tymczasowym. Domyślnie jest to katalog tymczasowy systemu; można go zmienić przez upload_tmp_dir. Jeśli skrypt nie przeniesie pliku, PHP usuwa go na końcu żądania.
Czy mogę ufać polu type w $_FILES?
Nie. Typ MIME zgłasza przeglądarka, a PHP według podręcznika go nie sprawdza. Także name i full_path pochodzą od nadawcy. Rzeczywisty typ pliku należy ustalić po stronie serwera na podstawie zawartości.
Dlaczego $_FILES jest puste, mimo że plik został wysłany?
Zwykle całe żądanie było większe niż post_max_size. Wtedy PHP całkowicie opróżnia $_POST i $_FILES, bez kodu błędu w tablicy. Inne przyczyny to brak enctype w formularzu lub wyłączone file_uploads.
Czy zakaz przesyłania plików w aplikacji pomaga przeciw PHP2Shell?
Nie. Przetwarzanie danych żądania odbywa się w PHP, zanim zacznie działać kod aplikacji. Skuteczne są aktualizacja PHP i restart wszystkich workerów; węższe limity to tylko środek przejściowy.
Jaka jest różnica między upload_max_filesize a post_max_size?
upload_max_filesize ogranicza każdy pojedynczy plik, a post_max_size całą zawartość żądania, łącznie ze wszystkimi plikami i polami. Dlatego post_max_size musi być większe niż upload_max_filesize.