Um upload recebe conteúdo controlado pelo visitante e, se for mal implementado, pode permitir execução de código, sobrescrita de arquivos, malware, XSS ou esgotamento de disco. Validar apenas a extensão ou o tipo enviado pelo navegador não é suficiente. Defina quem pode enviar, quais formatos são aceitos, tamanho, destino, retenção e forma de servir o arquivo.
Formulário HTML
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf" value="TOKEN_DA_SESSAO">
<label>Documento
<input type="file" name="documento" accept="application/pdf" required>
</label>
<button type="submit">Enviar</button>
</form>O atributo enctype="multipart/form-data" é obrigatório. accept ajuda a interface, mas pode ser contornado e não substitui validação no servidor.
Exemplo de processamento seguro
<?php
declare(strict_types=1);
session_start();
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
http_response_code(403);
exit('Solicitação inválida.');
}
$arquivo = $_FILES['documento'] ?? null;
if (!is_array($arquivo) || is_array($arquivo['error'] ?? null)) {
throw new RuntimeException('Upload inválido.');
}
if ($arquivo['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Falha no upload: ' . $arquivo['error']);
}
if ($arquivo['size'] > 5 * 1024 * 1024) {
throw new RuntimeException('Arquivo acima de 5 MB.');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($arquivo['tmp_name']);
$extensoes = ['application/pdf' => 'pdf'];
if (!isset($extensoes[$mime])) {
throw new RuntimeException('Formato não permitido.');
}
$nome = bin2hex(random_bytes(16)) . '.' . $extensoes[$mime];
$destino = dirname(__DIR__) . '/storage/uploads/' . $nome;
if (!move_uploaded_file($arquivo['tmp_name'], $destino)) {
throw new RuntimeException('Não foi possível armazenar o arquivo.');
}
O que o exemplo ainda precisa em produção?
- autenticação e autorização por usuário/objeto;
- token CSRF criado e rotacionado corretamente;
- tratamento amigável dos códigos
UPLOAD_ERR_*; - limite de quantidade e frequência;
- scanner de malware quando o risco justificar;
- registro de auditoria sem conteúdo sensível;
- política de retenção e exclusão;
- download por controlador autorizado ou armazenamento de objetos privado.
Não confie no nome e MIME do navegador
$_FILES['type'] e o nome original são fornecidos pelo cliente. Detecte o tipo pelo conteúdo com Fileinfo, mantenha uma allowlist pequena e gere um nome aleatório. Alguns formatos podem conter conteúdo ativo mesmo quando são válidos; imagens podem ser reprocessadas e documentos podem exigir sandbox/antivírus.
Armazene fora do diretório público
O ideal é guardar uploads fora do document root e entregá-los por código que valida autorização. Se precisarem ser públicos, use domínio estático sem execução de PHP, cabeçalhos seguros, nomes gerados e permissões mínimas. Nunca permita que um arquivo enviado se torne .php, .phtml, configuração do servidor ou substitua conteúdo existente.
Limites que participam do upload
| Camada | Exemplos |
|---|---|
| PHP | file_uploads, upload_max_filesize, post_max_size, max_file_uploads, max_input_time |
| Servidor web/proxy | limite de corpo e timeout |
| Aplicação | tamanho, quantidade, usuário e tipo |
| Conta | quota, inode, diretório temporário e permissões |
post_max_size precisa comportar a requisição completa e normalmente deve ser maior que upload_max_filesize. Aumentar limites globais pode facilitar abuso; altere somente o domínio/aplicação necessária.
Diagnóstico
- Registre o código de erro de
$_FILES. - Compare tamanho com limites PHP, proxy e aplicação.
- Confira espaço e escrita no diretório temporário e destino.
- Verifique a configuração da SAPI usada pelo site, não apenas CLI.
- Consulte log do PHP/servidor no horário exato.
- Teste arquivo pequeno permitido antes de elevar recursos.
Referências: uploads no manual PHP e move_uploaded_file(). Veja também configuração do php.ini.
Recursos relacionados
Continue com materiais e serviços relacionados ao tema deste guia.