Programação Por Equipe DevBras

Capa do artigo: Upload em PHP que falha calado: limite, código de erro e pasta que não executa

Upload em PHP que falha calado: limite, código de erro e pasta que não executa

Detecte quando o PHP descartou o upload antes do seu código, trate cada código de erro com uma mensagem clara e grave o arquivo fora da pasta pública, com nome gerado.

O formulário de comprovantes da Casa Pinho aceita "até 10 MB", segundo o if do código. A cliente envia um PDF de 12 MB e a tela responde "Selecione um arquivo", como se ela não tivesse escolhido nada. O PHP descartou o corpo inteiro antes de o script rodar, e o código interpretou o vazio como esquecimento. A resposta curta: antes de olhar o arquivo, detecte se o pedido estourou o limite do PHP; depois trate o código de erro de cada upload; por fim, grave com nome gerado numa pasta fora do alcance do servidor web.

Este guia foca em dois pontos que o tutorial clássico pula: o diagnóstico da falha silenciosa e o lugar onde o arquivo vai morar. Para calcular qual dos limites é o menor na sua pilha, veja o guia sobre limite de upload de verdade. Para validar o conteúdo a fundo, o de upload de arquivo em PHP.

Por que o upload some sem aviso

Um upload atravessa várias barreiras antes de chegar ao seu if. O servidor web pode recusar o corpo grande: o nginx responde 413 quando passa de client_max_body_size, e o Apache tem LimitRequestBody. Se passar, o PHP confere post_max_size, que vale para o corpo inteiro do POST. Quando esse limite é excedido, o PHP entrega $_POST e $_FILES vazios e só registra um aviso no log.

Se o corpo couber, cada arquivo ainda é comparado com upload_max_filesize. Aí o comportamento muda: o arquivo aparece em $_FILES, mas com um código de erro em vez de um arquivo temporário. Existe ainda max_file_uploads, que limita quantos arquivos vão num único pedido. O seu limite de 10 MB é o último da fila e só funciona se todos os anteriores forem maiores que ele.

São duas falhas diferentes. Na primeira, não há nada para inspecionar e o código precisa perceber pelo tamanho declarado do pedido. Na segunda, o código de erro diz exatamente o que aconteceu. O tutorial que vai direto para move_uploaded_file não trata nenhuma das duas.

Os códigos de erro que o PHP devolve

  • UPLOAD_ERR_OK (0): chegou inteiro.
  • UPLOAD_ERR_INI_SIZE (1): passou de upload_max_filesize.
  • UPLOAD_ERR_FORM_SIZE (2): passou do campo oculto MAX_FILE_SIZE do formulário, que o navegador controla e qualquer pessoa pode alterar.
  • UPLOAD_ERR_PARTIAL (3): a transferência foi interrompida no meio.
  • UPLOAD_ERR_NO_FILE (4): nenhum arquivo foi escolhido.
  • UPLOAD_ERR_NO_TMP_DIR (6), UPLOAD_ERR_CANT_WRITE (7) e UPLOAD_ERR_EXTENSION (8): problema do servidor, não da pessoa.

Os três últimos merecem log e alerta, porque nenhum usuário consegue resolvê-los tentando de novo.

Como receber o arquivo do jeito certo

  1. Confira se o pedido estourou post_max_size comparando o cabeçalho de tamanho com o limite configurado.
  2. Leia o código de erro do arquivo e traduza para uma frase que a pessoa entenda.
  3. Aplique o seu limite de negócio sobre o tamanho real.
  4. Descubra o tipo pelo conteúdo, não pela extensão nem pelo tipo que o navegador declarou.
  5. Gere o nome do arquivo e mova para uma pasta fora da raiz pública.
  6. Guarde o nome original só como dado, no banco, para exibir depois.

O código completo do recebimento

<?php
declare(strict_types=1);

const LIMITE_BYTES = 10 * 1024 * 1024;
const TIPOS_ACEITOS = [
    'application/pdf' => 'pdf',
    'image/jpeg'      => 'jpg',
    'image/png'       => 'png',
];
$pastaDestino = dirname(__DIR__) . '/storage/comprovantes';

function iniParaBytes(string $valor): int
{
    $valor = trim($valor);
    $numero = (int) $valor;
    return match (strtolower(substr($valor, -1))) {
        'g' => $numero * 1024 ** 3,
        'm' => $numero * 1024 ** 2,
        'k' => $numero * 1024,
        default => $numero,
    };
}

function responder(int $status, string $mensagem): never
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode(['mensagem' => $mensagem], JSON_UNESCAPED_UNICODE);
    exit;
}

$tamanhoPedido = (int) ($_SERVER['CONTENT_LENGTH'] ?? 0);
if (empty($_FILES) && $tamanhoPedido > iniParaBytes((string) ini_get('post_max_size'))) {
    responder(413, 'O arquivo é maior que o permitido (até 10 MB).');
}

$arquivo = $_FILES['comprovante'] ?? null;
if (!is_array($arquivo) || is_array($arquivo['error'])) {
    responder(400, 'Envie um único arquivo no campo de comprovante.');
}

switch ($arquivo['error']) {
    case UPLOAD_ERR_OK:
        break;
    case UPLOAD_ERR_INI_SIZE:
    case UPLOAD_ERR_FORM_SIZE:
        responder(413, 'O arquivo é maior que o permitido (até 10 MB).');
    case UPLOAD_ERR_PARTIAL:
        responder(400, 'O envio foi interrompido. Tente de novo.');
    case UPLOAD_ERR_NO_FILE:
        responder(400, 'Selecione um arquivo.');
    default:
        error_log('Upload falhou no servidor, código ' . $arquivo['error']);
        responder(500, 'Não conseguimos receber o arquivo agora.');
}

if ($arquivo['size'] > LIMITE_BYTES) {
    responder(413, 'O arquivo é maior que o permitido (até 10 MB).');
}

$tipo = (new finfo(FILEINFO_MIME_TYPE))->file($arquivo['tmp_name']);
if (!isset(TIPOS_ACEITOS[$tipo])) {
    responder(415, 'Envie PDF, JPG ou PNG.');
}

$nomeGerado = bin2hex(random_bytes(16)) . '.' . TIPOS_ACEITOS[$tipo];
$destino = $pastaDestino . '/' . $nomeGerado;

if (!move_uploaded_file($arquivo['tmp_name'], $destino)) {
    error_log('move_uploaded_file falhou para ' . $destino);
    responder(500, 'Não conseguimos guardar o arquivo agora.');
}

// grave $nomeGerado, $tipo, $arquivo['size'] e o nome original no banco
responder(201, 'Comprovante recebido.');

O teste de post_max_size vem primeiro porque é o único que olha para fora de $_FILES. A verificação is_array($arquivo['error']) recusa quem manda o campo como lista para confundir o código. E o switch não precisa de break depois de cada responder, porque a função termina o script e está declarada como never (PHP 8.1 ou mais novo).

A pasta onde o arquivo mora

Se a pasta de destino fica dentro da raiz pública, qualquer arquivo gravado ali pode ser pedido pela URL. Quando o servidor executa PHP naquela pasta, um foto.php enviado vira código rodando no seu servidor. Por isso o exemplo grava em storage/comprovantes, um nível acima da pasta pública. Arquivo que não tem URL não pode ser executado pela URL.

Para entregar o comprovante a quem tem direito, use um script que confere a permissão, busca o registro no banco e devolve o arquivo com Content-Type fixo, X-Content-Type-Options: nosniff e Content-Disposition: attachment. Assim o navegador baixa em vez de interpretar.

Quando a hospedagem não deixa sair da pasta pública

Em hospedagem compartilhada isso às vezes é impossível. No Apache 2.4, coloque um .htaccess na pasta de uploads que recuse qualquer arquivo com extensão de script:

<FilesMatch "(?i)\.(php[0-9]?|phtml|phar)$">
    Require all denied
</FilesMatch>
Options -Indexes

Teste de verdade: grave um teste.php com um echo nessa pasta e peça pela URL. Se aparecer o texto do echo, a barreira não está ativa. A resposta esperada é 403. O nome gerado continua obrigatório mesmo com essa regra, porque ele também impede que um envio sobrescreva o arquivo de outra pessoa.

Erros comuns

  • Tratar $_FILES vazio como "não escolheu arquivo": a pessoa reenvia o mesmo PDF grande várias vezes e desiste sem saber o motivo.
  • Confiar no campo MAX_FILE_SIZE: ele mora no HTML e qualquer pessoa altera; serve só como aviso antecipado.
  • Usar o nome original como caminho: abre espaço para sobrescrever arquivos, para nomes com ../ e para extensões executáveis.
  • Gravar dentro da raiz pública sem barreira: um script enviado como "imagem" pode rodar no servidor.
  • Checar o tipo pela extensão ou pelo type de $_FILES: os dois vêm do cliente e mentem com facilidade.
  • Mostrar o caminho interno no erro: a mensagem entrega a estrutura de pastas do servidor para quem está sondando.

Checklist

  • Um arquivo maior que post_max_size gera uma mensagem de tamanho, não "selecione um arquivo".
  • Cada código de erro de upload tem uma resposta própria, e os erros de servidor vão para o log.
  • O limite do seu código é menor que os limites do PHP e do servidor web.
  • O tipo é detectado pelo conteúdo com finfo.
  • O arquivo recebe nome gerado aleatoriamente.
  • A pasta de destino não é acessível pela URL, ou recusa scripts com resposta 403 testada.
  • O download passa por um script que confere permissão.

Envie agora um arquivo um pouco maior que o post_max_size do seu servidor: a mensagem que aparece explica o problema ou pede para escolher um arquivo que já foi escolhido?

Avalie este artigo

Referência: http://www.uolhost.com.br/faq/hospedagem/como-fazer-o-upload-de-arquivos-via-php.html