Em resumo: Quando um navegador envia um ficheiro, faz um pedido POST no formato multipart/form-data, em que os campos do formulário e o conteúdo dos ficheiros surgem como partes individuais, uma a seguir à outra. O PHP decompõe ele próprio este pedido, escreve cada parte de ficheiro num ficheiro temporário em upload_tmp_dir, regista os dados em $_FILES e só então inicia o seu script. O script move o ficheiro com move_uploaded_file(); tudo o que não for movido, o PHP elimina no fim do pedido.
O percurso de um ficheiro, do navegador ao script
Para quem decide, o mais importante é isto: um upload não é uma operação só da aplicação. Uma grande parte do trabalho acontece no interpretador PHP, antes de o código do CMS ou da loja intervir sequer. Por isso, os erros nesta parte do PHP afetam todos os sites, tenham ou não um formulário de upload.
O processo em resumo:
- O formulário com
method="POST"eenctype="multipart/form-data"é submetido. - O servidor web aceita o pedido e encaminha-o para o PHP, consoante o modo de operação através do PHP-FPM, do mod_php ou de outra interface.
- O PHP verifica limites como
post_max_sizee decompõe o conteúdo com base numa sequência separadora, a chamada boundary. - Os campos normais vão para
$_POST, as partes de ficheiro são escritas em ficheiros temporários e descritas em$_FILES. - Só agora o script arranca, verifica o ficheiro e move-o para o destino.
- No fim do pedido, o PHP elimina todos os ficheiros temporários que não foram movidos.
multipart/form-data: a estrutura de um pedido de upload
Um pedido de upload é texto simples com dados de ficheiro incorporados. O cabeçalho Content-Type indica a boundary e, no conteúdo, essa sequência separa as partes individuais. Cada parte tem os seus próprios cabeçalhos, com o nome do campo e, se for caso disso, o nome do ficheiro. Um exemplo muito abreviado e inofensivo:
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--
Todos os dados deste texto vêm do remetente, incluindo o nome do ficheiro e o Content-Type da parte. O PHP transfere-os para $_FILES, mas não verifica a sua plausibilidade.
O array $_FILES
Para um campo com o nome lebenslauf, o $_FILES['lebenslauf'] contém, segundo o manual do PHP, estas chaves:
| Chave | Conteúdo | Merece confiança? |
|---|---|---|
name |
nome original do ficheiro no remetente | não |
full_path |
caminho comunicado pelo navegador, desde o PHP 8.1.0 | não |
type |
tipo MIME segundo o navegador, não verificado pelo PHP | não |
size |
tamanho em bytes | determinado pelo PHP |
tmp_name |
caminho do ficheiro temporário no servidor | atribuído pelo PHP |
error |
código de erro do processamento do upload | definido pelo PHP |
Nos campos com parênteses retos no nome, por exemplo bilder[], cada chave contém um array com uma entrada por ficheiro. A aplicação percorre então, por exemplo, $_FILES['bilder']['error'] e acede a tmp_name e name através do mesmo índice.
Um formulário pode ainda enviar, antes do campo de ficheiro, um campo oculto MAX_FILE_SIZE. Segundo o manual, é apenas uma indicação para o navegador, que se contorna com facilidade. Só pode confiar nos limites que o servidor impõe: no PHP, no servidor web e na própria aplicação.
Limites: as diretivas do php.ini para uploads
Estas diretivas controlam o processamento de uploads. Os valores predefinidos provêm do manual do PHP e aplicam-se quando nenhum php.ini os substitui.
| Diretiva | Predefinição | Alterável em |
|---|---|---|
file_uploads |
1 |
apenas configuração do sistema |
upload_tmp_dir |
vazio (temp do sistema) | apenas configuração do sistema |
upload_max_filesize |
2M |
php.ini, pool, configuração por diretório |
post_max_size |
8M |
php.ini, pool, configuração por diretório |
max_file_uploads |
20 |
php.ini, pool, configuração por diretório |
enable_post_data_reading |
1 |
php.ini, pool, configuração por diretório |
O post_max_size tem de ser maior do que o upload_max_filesize e, segundo o manual, o memory_limit deve em regra ser maior do que o post_max_size. Se o upload_tmp_dir não for gravável, o PHP recorre à pasta temporária do sistema. Com open_basedir, a pasta temporária tem de estar autorizada, caso contrário os uploads falham. Os valores práticos e as suas consequências são descritos no artigo Limitar post_max_size e upload_max_filesize de forma sensata.
Avaliar corretamente os códigos de erro
O campo error diz o que aconteceu durante o processamento. As constantes vão de UPLOAD_ERR_OK (0), passando por UPLOAD_ERR_INI_SIZE (1), UPLOAD_ERR_FORM_SIZE (2), UPLOAD_ERR_PARTIAL (3) e UPLOAD_ERR_NO_FILE (4), até UPLOAD_ERR_NO_TMP_DIR (6), UPLOAD_ERR_CANT_WRITE (7) e 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);
}
O exemplo mostra apenas a mecânica. A verificação do tipo de ficheiro, o armazenamento fora da raiz web e o bloqueio da execução de scripts são descritos no guia Implementar uploads de ficheiros em PHP em segurança.
Uploads e PHP2Shell: o que significam as medidas provisórias
As vulnerabilidades PHP2Shell CVE-2026-109618 e CVE-2026-109619 dizem respeito ao processamento em torno do pedido, no interpretador PHP. Por isso, o php.net indica como medidas provisórias, entre outras, post_max_size = 8M (sem uploads) ou 32M (com uploads), upload_max_filesize abaixo desse valor, e ainda session.upload_progress.enabled=0. Estes valores pertencem ao php.ini, ao pool FPM ou à configuração do servidor, e depois todos os workers têm de ser reiniciados.
Autores especializados sugerem ainda bloquear, no proxy ou na WAF, os pedidos multipart/form-data fora dos caminhos de upload reais. É uma medida não oficial, com o risco de estragar formulários; a ponderação é descrita no artigo Filtrar pedidos de upload na periferia. Nenhuma destas medidas substitui a atualização para o PHP 8.5.12, 8.4.27, 8.3.36 ou 8.2.35.
Como ajuda concretamente a PHP SOS?
- Determinamos quais dos seus sites precisam realmente de uploads e em que caminhos.
- Definimos os limites e as medidas provisórias por pool, reiniciamos todos os workers e testamos os formulários.
- Verificamos as pastas de upload quanto a ficheiros executáveis e bloqueamos a execução de scripts no âmbito do hardening do servidor.
- Aplicamos a atualização do PHP e documentamos o nível de patches de cada site.
Perguntas frequentes
Onde guarda o PHP primeiro os ficheiros enviados?
Numa pasta temporária. Por predefinição é a pasta temporária do sistema; com upload_tmp_dir pode ser alterada. Se o ficheiro não for movido no script, o PHP elimina-o no fim do pedido.
Posso confiar no campo type de $_FILES?
Não. O tipo MIME é comunicado pelo navegador e, segundo o manual, o PHP não o verifica. Também name e full_path vêm do remetente. O tipo real do ficheiro determina-se no servidor, com base no conteúdo.
Porque é que $_FILES está vazio, apesar de ter sido enviado um ficheiro?
Na maioria dos casos, o pedido completo era maior do que post_max_size. Nesse caso, o PHP esvazia por completo $_POST e $_FILES, sem código de erro no array. Outras causas são a falta de enctype no formulário ou file_uploads desativado.
Ajuda contra o PHP2Shell proibir uploads na aplicação?
Não. O processamento dos dados do pedido acontece no PHP antes de o código da aplicação ser executado. O que funciona é a atualização do PHP e o reinício de todos os workers; limites mais apertados são apenas uma medida provisória.
Qual é a diferença entre upload_max_filesize e post_max_size?
O upload_max_filesize limita cada ficheiro individual, o post_max_size limita todo o conteúdo do pedido, incluindo todos os ficheiros e campos. Por isso, o post_max_size tem de ser maior do que o upload_max_filesize.