Upload de arquivos com segurança
1. Introdução aos Riscos de Segurança no Upload de Arquivos
O upload de arquivos é uma das funcionalidades mais críticas em aplicações PHP, pois expõe diretamente o servidor a ataques. As principais vulnerabilidades incluem:
- Execução remota de código: Um atacante envia um arquivo PHP disfarçado de imagem, obtendo acesso ao servidor
- Path traversal: Exploração de nomes de arquivo maliciosos como
../../../etc/passwd - Negação de serviço (DoS): Upload de arquivos extremamente grandes ou infinitos
As consequências podem ser devastadoras: desde a instalação de shells reversos até o vazamento completo do banco de dados. Diferentemente de frameworks que abstraem essas preocupações, o PHP puro exige que o desenvolvedor implemente cada camada de proteção manualmente.
2. Configuração do Ambiente e Validação Inicial
Antes de qualquer código, configure o php.ini com limites realistas:
upload_max_filesize = 10M
post_max_size = 12M
max_file_uploads = 5
A validação começa com o código de erro em $_FILES:
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
die('Método não permitido');
}
if ($_FILES['arquivo']['error'] !== UPLOAD_ERR_OK) {
$mensagens = [
UPLOAD_ERR_INI_SIZE => 'Arquivo excede o limite do php.ini',
UPLOAD_ERR_FORM_SIZE => 'Arquivo excede o limite do formulário',
UPLOAD_ERR_PARTIAL => 'Upload parcial',
UPLOAD_ERR_NO_FILE => 'Nenhum arquivo enviado'
];
die($mensagens[$_FILES['arquivo']['error']] ?? 'Erro desconhecido');
}
Sempre utilize is_uploaded_file() e move_uploaded_file():
$arquivo = $_FILES['arquivo']['tmp_name'];
if (!is_uploaded_file($arquivo)) {
die('Arquivo não foi enviado via HTTP POST');
}
$destino = '/var/uploads/' . gerarNomeSeguro();
if (!move_uploaded_file($arquivo, $destino)) {
die('Falha ao mover arquivo');
}
3. Validação do Tipo de Arquivo (MIME e Extensão)
Nunca confie em $_FILES['arquivo']['type'] — este valor é enviado pelo cliente e pode ser falsificado. Use finfo para detectar o MIME real:
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['arquivo']['tmp_name']);
$mimesPermitidos = [
'image/jpeg',
'image/png',
'image/gif',
'application/pdf'
];
if (!in_array($mime, $mimesPermitidos)) {
die('Tipo de arquivo não permitido');
}
Implemente uma whitelist de extensões (nunca blacklist):
$extensao = strtolower(pathinfo($_FILES['arquivo']['name'], PATHINFO_EXTENSION));
$extensoesPermitidas = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
if (!in_array($extensao, $extensoesPermitidas)) {
die('Extensão não permitida');
}
// Verificação dupla: extensão + MIME + magic bytes
$magicBytes = [
'jpg' => "\xFF\xD8\xFF",
'png' => "\x89\x50\x4E\x47",
'gif' => "\x47\x49\x46",
'pdf' => "\x25\x50\x44\x46"
];
$handle = fopen($_FILES['arquivo']['tmp_name'], 'rb');
$bytes = fread($handle, 3);
fclose($handle);
if (strpos($bytes, $magicBytes[$extensao]) !== 0) {
die('Assinatura de arquivo inválida');
}
4. Sanitização do Nome do Arquivo e Prevenção de Path Traversal
Nunca use o nome original do arquivo. Remova caracteres perigosos e gere identificadores únicos:
function gerarNomeSeguro(): string {
$extensao = strtolower(pathinfo($_FILES['arquivo']['name'], PATHINFO_EXTENSION));
$nome = hash('sha256', uniqid(mt_rand(), true) . $_FILES['arquivo']['tmp_name']);
return $nome . '.' . $extensao;
}
// Sanitização adicional do nome original (apenas para logging)
$nomeOriginal = preg_replace(
'/[^a-zA-Z0-9._-]/',
'_',
basename($_FILES['arquivo']['name'])
);
Configure o diretório de destino sem permissão de execução:
mkdir -p /var/uploads
chmod 0755 /var/uploads
Use realpath() para garantir que o destino está dentro do diretório permitido:
$diretorioBase = realpath('/var/uploads');
$destino = $diretorioBase . '/' . gerarNomeSeguro();
if (strpos($destino, $diretorioBase) !== 0) {
die('Tentativa de path traversal detectada');
}
5. Limitação de Tamanho e Prevenção de Negação de Serviço (DoS)
Além das configurações do php.ini, valide o tamanho no servidor:
$tamanhoMaximo = 10 * 1024 * 1024; // 10 MB
if ($_FILES['arquivo']['size'] > $tamanhoMaximo) {
die('Arquivo muito grande');
}
// Controle de cota por sessão
session_start();
if (!isset($_SESSION['upload_total'])) {
$_SESSION['upload_total'] = 0;
}
$cotaMaxima = 50 * 1024 * 1024; // 50 MB por sessão
if ($_SESSION['upload_total'] + $_FILES['arquivo']['size'] > $cotaMaxima) {
die('Cota de upload excedida');
}
$_SESSION['upload_total'] += $_FILES['arquivo']['size'];
Para imagens, verifique dimensões reais:
if (strpos($mime, 'image/') === 0) {
$dimensoes = getimagesize($_FILES['arquivo']['tmp_name']);
if (!$dimensoes) {
die('Arquivo de imagem inválido ou corrompido');
}
$larguraMaxima = 4000;
$alturaMaxima = 4000;
if ($dimensoes[0] > $larguraMaxima || $dimensoes[1] > $alturaMaxima) {
die('Dimensões de imagem excedem o limite');
}
}
// Timeout para uploads grandes
set_time_limit(30);
6. Armazenamento Seguro e Tratamento de Imagens
Armazene arquivos fora da raiz pública do servidor. Crie um script intermediário para servir arquivos:
// download.php
$arquivo = basename($_GET['file']);
$caminho = '/var/uploads/' . $arquivo;
if (!file_exists($caminho)) {
http_response_code(404);
die('Arquivo não encontrado');
}
header('Content-Type: ' . mime_content_type($caminho));
header('Content-Disposition: attachment; filename="' . $arquivo . '"');
header('Content-Length: ' . filesize($caminho));
readfile($caminho);
Regenere imagens para remover metadados maliciosos:
function regenerarImagem(string $origem, string $destino, string $mime): bool {
switch ($mime) {
case 'image/jpeg':
$img = imagecreatefromjpeg($origem);
$salvou = imagejpeg($img, $destino, 85);
break;
case 'image/png':
$img = imagecreatefrompng($origem);
$salvou = imagepng($img, $destino, 6);
break;
case 'image/gif':
$img = imagecreatefromgif($origem);
$salvou = imagegif($img, $destino);
break;
default:
return false;
}
imagedestroy($img);
return $salvou;
}
Desative execução de scripts no diretório de uploads (Apache - .htaccess):
Options -ExecCGI -Indexes
RemoveHandler .php .phtml .php3 .php4 .php5
RemoveType .php .phtml .php3 .php4 .php5
php_flag engine off
7. Boas Práticas Adicionais e Logging
Implemente logging detalhado de todas as tentativas:
function logUpload(string $status, array $dados): void {
$log = sprintf(
"[%s] IP: %s | Status: %s | Arquivo: %s | Tamanho: %d | MIME: %s\n",
date('Y-m-d H:i:s'),
$_SERVER['REMOTE_ADDR'],
$status,
$dados['nome_original'] ?? 'N/A',
$dados['tamanho'] ?? 0,
$dados['mime'] ?? 'N/A'
);
file_put_contents('/var/log/uploads.log', $log, FILE_APPEND | LOCK_EX);
}
Implemente rate limiting simples:
session_start();
$limite = 5; // uploads por minuto
$janela = 60; // segundos
if (!isset($_SESSION['upload_timestamps'])) {
$_SESSION['upload_timestamps'] = [];
}
$_SESSION['upload_timestamps'] = array_filter(
$_SESSION['upload_timestamps'],
fn($t) => $t > (time() - $janela)
);
if (count($_SESSION['upload_timestamps']) >= $limite) {
die('Muitos uploads. Tente novamente em ' . ($_SESSION['upload_timestamps'][0] + $janela - time()) . ' segundos');
}
$_SESSION['upload_timestamps'][] = time();
Teste sua implementação com arquivos maliciosos como shell.php.jpg:
// O sistema deve rejeitar porque:
// 1. A extensão .jpg está na whitelist
// 2. O MIME real será text/plain ou application/x-php
// 3. Os magic bytes não corresponderão a JPEG
// 4. A regeneração da imagem falhará (não é uma imagem real)
Referências
- PHP Manual: Handling File Uploads — Documentação oficial sobre upload de arquivos em PHP, incluindo
$_FILES,move_uploaded_file()e configurações dophp.ini - OWASP: Unrestricted File Upload — Guia completo da OWASP sobre vulnerabilidades de upload irrestrito e como mitigá-las
- PHP Security Consortium: File Upload Security — Boas práticas de segurança para upload de arquivos, incluindo validação MIME e prevenção de path traversal
- Mozilla MDN: File input security — Considerações de segurança no lado do cliente e servidor para uploads via HTML
- PHP Fig: PSR-7 Uploaded File Interface — Especificação PSR-7 para manipulação segura de arquivos enviados em requisições HTTP
- Sucuri Blog: How to Prevent Malicious File Uploads — Artigo técnico sobre prevenção de uploads maliciosos com exemplos práticos em PHP
- PHP Internals: Fileinfo Extension — Documentação da extensão
finfopara detecção de MIME type baseada no conteúdo real do arquivo