Aumente o tamanho de upload no PHP e veja qual limite o rejeitou
Para aceitar um arquivo de 20 MiB, defina upload_max_filesize = 20M, dê mais espaço à requisição
multipart completa com post_max_size = 25M e garanta que sua aplicação e seu servidor web
também permitam isso. Alterar as configurações do PHP não consegue sobrepor um limite menor no seu
handler de upload. Este guia mostra como encontrar as configurações ativas e reproduzir cada falha
via HTTP com um pequeno endpoint local.
Como verificar o tamanho máximo de upload de arquivos no PHP
Verifique o processo PHP que atende à URL de upload. php --ini informa a
configuração da linha de comando; isso não indica o que o FPM ou o Apache carregou. O PHP pode usar
arquivos de configuração diferentes para diferentes interfaces de servidor.
Salve isto como check_upload_size.php ao lado do seu endpoint de upload e acesse-o pelo
mesmo site. Em um site existente, restrinja o acesso a administradores e remova o arquivo após o
diagnóstico: os caminhos de arquivo são úteis para você, mas não devem ser públicos.
<?php
header('Content-Type: application/json');
echo json_encode([
'version' => PHP_VERSION,
'sapi' => PHP_SAPI,
'loaded_ini' => php_ini_loaded_file(),
'scanned_ini' => php_ini_scanned_files(),
'file_uploads' => ini_get('file_uploads'),
'upload_max_filesize' => ini_get('upload_max_filesize'),
'post_max_size' => ini_get('post_max_size'),
'max_file_uploads' => ini_get('max_file_uploads'),
], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
O PHP documenta os padrões 2M para upload_max_filesize,
8M para post_max_size e 20 para
max_file_uploads; as instalações podem sobrescrevê-los. Os dois primeiros são limites
em bytes, enquanto max_file_uploads limita o número de arquivos. Na notação de tamanho
do PHP, 20M significa 20 × 1.024 × 1.024 bytes, ou 20 MiB. Veja as
diretivas principais e o
parser de tamanhos.
| Camada | O que limita | O que procurar |
|---|---|---|
| Servidor web ou proxy | O corpo da requisição recebida | Uma rejeição antes de o handler PHP executar; inspecione os logs desse servidor |
post_max_size | O corpo POST inteiro, incluindo campos e o overhead do multipart | Tanto $_POST quanto $_FILES podem ficar vazios |
upload_max_filesize | Cada arquivo enviado | UPLOAD_ERR_INI_SIZE no campo error do arquivo |
| Validação da aplicação | O arquivo que sua aplicação aceitará | Uma constante do handler ou regra do framework que pode ser menor que o limite do PHP |
O PHP documenta o comportamento de superglobais vazias
e os códigos de erro de upload.
Um $_FILES vazio, por si só, não prova que a requisição era grande demais: um
campo de arquivo ausente também pode causar isso.
Como aumentar o tamanho de upload de arquivos no PHP
Edite a configuração usada pela requisição web, usando a saída de diagnóstico acima para localizá-la.
No Ubuntu, uma instalação empacotada do PHP 8.3 costuma usar /etc/php/8.3/fpm/php.ini para o
FPM ou /etc/php/8.3/apache2/php.ini para o mod_php. O caminho informado e os valores efetivos têm
precedência sobre esses exemplos.
file_uploads = On
upload_max_filesize = 20M
post_max_size = 25M
display_errors = Off
log_errors = On
Recarregue o serviço que executa o PHP depois de alterar a configuração. Com FPM, isso significa o
serviço FPM correspondente; com mod_php, significa o Apache. Acesse a URL de diagnóstico novamente e
verifique os valores efetivos. Essas duas diretivas de tamanho são configurações
INI_PERDIR, então ini_set() dentro de
upload.php não consegue aumentá-las. Desativar a exibição de erros na
configuração também mantém os avisos emitidos durante o parsing da requisição fora da resposta JSON.
Usando a configuração do PHP-FPM
Um pool pode sobrescrever php.ini. Verifique o pool que atende a este site se
os valores ainda forem diferentes. Para um limite de 20 MiB, as configurações de pool correspondentes
são php_admin_value[upload_max_filesize] = 20M e php_admin_value[post_max_size] = 25M.
A referência de configuração do FPM
explica as substituições por pool. Recarregue o serviço FPM afetado e verifique novamente via HTTP.
Usando .htaccess (para Apache 2.4+ com mod_php)
Para PHP executado como módulo do Apache, php_value upload_max_filesize 20M e
php_value post_max_size 25M podem ir em .htaccess quando o servidor permite
essas substituições. Elas não são configurações do PHP-FPM; não as adicione só porque o Apache fica
na frente do site. Veja as instruções de configuração do Apache na
documentação do PHP.
Se a requisição nunca chegar ao PHP, verifique o limite de corpo upstream. O
client_max_body_size do Nginx
tem como padrão 1m e retorna 413 para uma requisição grande demais. Para
este exemplo, client_max_body_size 25m; permite a requisição multipart pretendida. O Apache tem
LimitRequestBody, e um proxy de hospedagem
pode impor outro limite. Reserve espaço para a requisição inteira em todas as camadas. Aumentar um
limite do PHP não altera nenhuma dessas configurações.
Implementando upload de arquivos no PHP
Use um ambiente Linux local com PHP 8.3 ou posterior de 64 bits, a extensão Fileinfo e cURL. O exemplo foi testado com PHP 8.3.30 e 8.5.10. O servidor embutido do PHP serve para desenvolvimento local. Ele não reproduz uma implantação com FPM, Apache ou proxy.
Crie um diretório novo. Os comandos encadeados param se o diretório já existir ou se a navegação falhar; escolha outro nome em vez de sobrescrever um projeto existente.
mkdir php-upload-demo &&
cd php-upload-demo &&
mkdir public private &&
chmod 700 private
Continue somente depois que isso funcionar. Salve o bloco INI acima como
php-upload.ini neste diretório. Salve o script de diagnóstico como
public/check_upload_size.php.
Script de upload em PHP
Salve este handler completo como public/upload.php. APP_MAX_BYTES é o
limite independente da aplicação; aqui ele coincide com o limite de 20 MiB por arquivo do PHP. O
handler aceita arquivos JPEG, PNG e PDF, confere o tipo MIME detectado com a extensão do nome do
arquivo e mantém os bytes aceitos em private/, fora da raiz de documentos.
<?php
declare(strict_types=1);
const APP_MAX_BYTES = 20 * 1024 * 1024;
header('Content-Type: application/json');
function rejectUpload(int $status, string $code, string $message): never {
http_response_code($status);
echo json_encode([
'success' => false,
'code' => $code,
'message' => $message,
], JSON_THROW_ON_ERROR);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Allow: POST');
rejectUpload(405, 'method', 'Send a multipart POST request.');
}
$postLimit = ini_parse_quantity(ini_get('post_max_size'));
$contentLength = $_SERVER['CONTENT_LENGTH'] ?? null;
if ($postLimit > 0 && $contentLength !== null && (int) $contentLength > $postLimit) {
rejectUpload(413, 'post_max_size', 'The complete request exceeds PHP post_max_size.');
}
$file = $_FILES['file'] ?? null;
if (!is_array($file) || array_keys($_FILES) !== ['file'] ||
!isset($file['error'], $file['size'], $file['name'], $file['tmp_name']) ||
!is_int($file['error']) || !is_int($file['size']) ||
!is_string($file['name']) || !is_string($file['tmp_name'])) {
rejectUpload(400, 'invalid_upload', 'Send one file in the file field, without array brackets.');
}
if ($file['error'] !== UPLOAD_ERR_OK) {
[$status, $code, $message] = match ($file['error']) {
UPLOAD_ERR_INI_SIZE => [413, 'upload_max_filesize', 'The file exceeds PHP upload_max_filesize.'],
UPLOAD_ERR_FORM_SIZE => [413, 'form_limit', 'The file exceeds the submitted MAX_FILE_SIZE.'],
UPLOAD_ERR_PARTIAL => [400, 'partial_upload', 'The file arrived incomplete. Retry the upload.'],
UPLOAD_ERR_NO_FILE => [400, 'missing_file', 'Choose a file to upload.'],
default => [500, 'upload_unavailable', 'The upload service is unavailable.'],
};
rejectUpload($status, $code, $message);
}
if (!is_uploaded_file($file['tmp_name'])) {
rejectUpload(400, 'invalid_upload', 'The file is not a valid HTTP upload.');
}
$directory = null;
$destination = null;
try {
$size = filesize($file['tmp_name']);
if ($size === false) {
throw new RuntimeException('Cannot measure upload');
}
if ($size === 0) {
rejectUpload(400, 'empty_file', 'The file is empty.');
}
if ($size > APP_MAX_BYTES) {
rejectUpload(413, 'application_limit', 'The file exceeds the application size limit.');
}
$allowed = [
'image/jpeg' => ['jpg', 'jpeg'],
'image/png' => ['png'],
'application/pdf' => ['pdf'],
];
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!isset($allowed[$mime]) || !in_array($extension, $allowed[$mime], true)) {
rejectUpload(415, 'file_type', 'Send a JPEG, PNG, or PDF with a matching extension.');
}
$hash = hash_file('sha256', $file['tmp_name']);
if ($hash === false) {
throw new RuntimeException('Cannot hash upload');
}
$id = bin2hex(random_bytes(16));
$candidate = dirname(__DIR__) . '/private/' . $id;
// Creating a new directory reserves this ID without overwriting an earlier upload.
if (!@mkdir($candidate, 0700)) {
throw new RuntimeException('Cannot reserve storage');
}
$directory = $candidate;
$destination = $directory . '/file.' . $allowed[$mime][0];
if (!@move_uploaded_file($file['tmp_name'], $destination) || !@chmod($destination, 0600)) {
throw new RuntimeException('Cannot store upload');
}
http_response_code(201);
echo json_encode([
'success' => true,
'id' => $id,
'type' => $mime,
'size' => $size,
'sha256' => $hash,
], JSON_THROW_ON_ERROR);
} catch (Throwable $error) {
if ($destination !== null) {
@unlink($destination);
}
if ($directory !== null) {
@rmdir($directory);
}
error_log('Upload failed: ' . get_class($error));
rejectUpload(500, 'upload_unavailable', 'The upload service is unavailable.');
}
A verificação de tamanho da requisição usa Content-Length para distinguir um corpo
sabidamente grande demais de um arquivo ausente. As requisições cURL abaixo informam esse tamanho.
Sem ele, o handler não consegue diagnosticar dessa forma um estouro de post_max_size;
imponha um limite de corpo no servidor web para clientes que usam requisições em streaming.
ini_parse_quantity() trata os sufixos de tamanho do PHP, incluindo um limite zero, que
desativa o limite de tamanho de POST multipart.
Inicie o servidor a partir de php-upload-demo. Escolha uma porta disponível e use a
mesma porta nas requisições seguintes. Este comando serve apenas public/:
php -c php-upload.ini -S 127.0.0.1:8080 -t public
Deixe esse terminal em execução. Em um segundo terminal, abra o mesmo diretório do projeto e verifique:
curl -fsS http://127.0.0.1:8080/check_upload_size.php
Espere que sapi seja cli-server,
upload_max_filesize seja 20M e post_max_size
seja 25M. O caminho do INI carregado deve apontar para este projeto. Pare o
servidor com Ctrl+C ao terminar.
Envie requisições multipart reais
Para testar tamanhos de forma reproduzível, salve isto como make-probe.php na raiz do
projeto. Ele cria um PNG válido de um pixel, preenchido até o tamanho em bytes solicitado. O
preenchimento é proposital: ele testa limites de bytes sem exigir uma fotografia grande e demonstra
por que a detecção de MIME não é um veredito de segurança. O modo de criação exclusiva se recusa a
sobrescrever um arquivo de teste existente.
<?php
declare(strict_types=1);
$bytes = filter_var($argv[2] ?? '', FILTER_VALIDATE_INT);
if ($argc !== 3 || $bytes === false || $bytes < 1024 || $bytes > 30 * 1024 * 1024) {
fwrite(STDERR, "Usage: php make-probe.php OUTPUT BYTES (1024 to 31457280)\n");
exit(1);
}
$png = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg==', true);
$output = @fopen($argv[1], 'xb');
if ($output === false) {
fwrite(STDERR, "Cannot create probe; choose a new output filename.\n");
exit(1);
}
if (fwrite($output, $png) !== strlen($png) || !ftruncate($output, $bytes) || !fclose($output)) {
fwrite(STDERR, "Cannot finish probe.\n");
exit(1);
}
Crie e envie um arquivo de 6 MiB. -F fornece a codificação multipart e o
boundary; não defina o cabeçalho Content-Type manualmente.
-i mostra o status da resposta. Essas chamadas de diagnóstico omitem o
--fail do cURL para que você possa ler o corpo JSON das respostas de erro
esperadas.
php make-probe.php probe-6.png 6291456 &&
curl -sS -i -F 'file=@probe-6.png' http://127.0.0.1:8080/upload.php
Espere HTTP 201 com success: true, type: "image/png",
size: 6291456, um id aleatório e um
sha256. Os bytes permanecem em private/<id>/file.png; não há URL
pública de download. Compare o hash da resposta com um hash local do arquivo de origem:
php -r 'echo hash_file("sha256", "probe-6.png"), PHP_EOL;'
Repita com o limite exato de 20 MiB e depois com arquivos grandes o suficiente para acionar cada limite do PHP:
php make-probe.php probe-20.png 20971520 &&
curl -sS -i -F 'file=@probe-20.png' http://127.0.0.1:8080/upload.php
php make-probe.php probe-21.png 22020096 &&
curl -sS -i -F 'file=@probe-21.png' http://127.0.0.1:8080/upload.php
php make-probe.php probe-26.png 27262976 &&
curl -sS -i -F 'file=@probe-26.png' http://127.0.0.1:8080/upload.php
| Arquivo | Resultado esperado com as configurações documentadas |
|---|---|
| PNG de 6 MiB | 201; armazenado com bytes e hash correspondentes |
| PNG de 20 MiB | 201; o limite por arquivo é inclusivo |
| PNG de 21 MiB | 413 com code: "upload_max_filesize" |
| PNG de 26 MiB | 413 com code: "post_max_size"; a requisição completa é grande demais |
Para isolar o limite da aplicação, pare o servidor e reinicie-o com esta substituição temporária:
php -c php-upload.ini -d upload_max_filesize=24M -S 127.0.0.1:8080 -t public
Envie novamente o arquivo de teste de 21 MiB existente:
curl -sS -i -F 'file=@probe-21.png' http://127.0.0.1:8080/upload.php
Agora espere 413 com code: "application_limit": o PHP permite o arquivo, mas
APP_MAX_BYTES o rejeita. Depois, restaure o comando original do servidor. Se uma
aplicação existente ainda rejeitar um arquivo de 6 MiB depois que você aumentar os limites do PHP,
procure um limite menor na aplicação, como 5242880 bytes, ou uma regra de
validação do framework. Alterar php.ini não atualiza essa regra.
Os comandos que criam arquivos de teste nunca substituem um arquivo de teste existente. Para reenviar um deles, execute apenas o respectivo comando cURL. Cada requisição bem-sucedida cria um novo upload privado, mesmo que o conteúdo seja idêntico; falhas não deixam nenhum upload retido. A demonstração mantém os uploads bem-sucedidos até você remover os dados privados dela.
Boas práticas para uploads de arquivos seguros
Segurança de diretórios
Mantenha private/ fora do diretório servido e pertencente à conta do PHP. Cada
diretório de upload reservado tem modo 0700; os arquivos armazenados têm
modo 0600. O nome original do arquivo nunca vira um caminho de
armazenamento. O move_uploaded_file() do PHP
verifica se a origem foi enviada pelo PHP, mas pode sobrescrever um destino existente.
Reservar um diretório aleatório novo antes de mover o arquivo evita essa sobrescrita neste exemplo.
Antes de expor um endpoint de upload
Este exemplo local não tem autenticação, cotas por usuário, scanner de malware nem entrega pública de arquivos. O Fileinfo identifica um tipo provável a partir dos bytes; nem esse resultado nem um hash SHA-256 comprova que um arquivo é inofensivo. Em uma aplicação implantada, decida quem pode enviar arquivos, limite o uso de armazenamento de cada um e valide ou escaneie o conteúdo antes de liberá-lo. Frameworks e indicadores de progresso de upload continuam sujeitos aos mesmos limites de requisição e de arquivo; eles não os eliminam.
