Upload de Arquivos com PHP: Validação e Segurança
Upload de arquivos é uma funcionalidade comum em aplicações web, mas também representa um dos maiores vetores de ataque se implementado incorretamente. Neste artigo, abordaremos desde o formulário HTML até técnicas avançadas de validação, focando sempre em segurança. Vou compartilhar a experiência que acumulei em anos trabalhando com essa feature crítica.
Fundamentos: HTML e Configuração do PHP
O Formulário HTML Correto
O formulário deve usar enctype="multipart/form-data" para transmitir arquivos corretamente. Sem isso, o servidor não recebe os dados do upload.
<form method="POST" enctype="multipart/form-data">
<input type="file" name="arquivo" accept=".pdf,.jpg,.png" required>
<input type="hidden" name="MAX_FILE_SIZE" value="5242880">
<button type="submit">Enviar</button>
</form>
O atributo accept é apenas uma validação no cliente — nunca confie nela. O MAX_FILE_SIZE é processado pelo PHP, mas também adicione validação no servidor.
Configurações PHP Essenciais
Certifique-se de que seu php.ini está configurado corretamente:
upload_max_filesize = 10M
post_max_size = 10M
upload_tmp_dir = /tmp/php_uploads
file_uploads = On
Mantenha upload_tmp_dir em uma partição separada e com permissões restritas (755 ou 750). Nunca deixe como padrão do sistema.
Validação Segura no Servidor
Validação de Tipo de Arquivo
A validação mais comum — verificar a extensão — é frágil e perigosa. Um usuário mal-intencionado pode renomear um .php para .jpg e contornar essa verificação. Use a função mime_content_type() ou melhor ainda, a extensão fileinfo:
<?php
$arquivo = $_FILES['arquivo'] ?? null;
if (!$arquivo) {
die('Nenhum arquivo enviado.');
}
// Validar tamanho
$maxSize = 5 * 1024 * 1024; // 5MB
if ($arquivo['size'] > $maxSize) {
die('Arquivo muito grande.');
}
// Validar tipo MIME com fileinfo
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $arquivo['tmp_name']);
finfo_close($finfo);
$mimePermitidos = ['image/jpeg', 'image/png', 'application/pdf'];
if (!in_array($mimeType, $mimePermitidos, true)) {
die('Tipo de arquivo não permitido.');
}
Validação de Conteúdo
Para imagens, use getimagesize() para garantir que é realmente uma imagem válida:
<?php
if ($mimeType === 'image/jpeg' || $mimeType === 'image/png') {
$imageInfo = @getimagesize($arquivo['tmp_name']);
if ($imageInfo === false) {
die('Imagem inválida ou corrompida.');
}
if ($imageInfo[0] > 4000 || $imageInfo[1] > 4000) {
die('Dimensões da imagem excedem o limite.');
}
}
Para PDFs, valide o cabeçalho do arquivo:
<?php
if ($mimeType === 'application/pdf') {
$handle = fopen($arquivo['tmp_name'], 'rb');
$header = fread($handle, 4);
fclose($handle);
if ($header !== "%PDF") {
die('Arquivo PDF inválido.');
}
}
Armazenamento Seguro
Diretório Protegido Fora da Raiz Web
Nunca armazene uploads na raiz pública (/public_html). Crie um diretório fora dela:
/var/www/
├── html/ (raiz web pública)
├── uploads/ (armazenamento seguro - NÃO acessível via HTTP)
└── ...
Configure as permissões:
mkdir -p /var/www/uploads
chmod 750 /var/www/uploads
chown www-data:www-data /var/www/uploads
Renomeação Segura do Arquivo
Nunca use o nome original do arquivo. Gere um nome único e seguro:
<?php
$nomeOriginal = basename($arquivo['name']);
$extensao = strtolower(pathinfo($nomeOriginal, PATHINFO_EXTENSION));
// Gerar nome único
$novoNome = bin2hex(random_bytes(16)) . '.' . $extensao;
$caminhoDestino = '/var/www/uploads/' . $novoNome;
if (!move_uploaded_file($arquivo['tmp_name'], $caminhoDestino)) {
die('Erro ao mover arquivo.');
}
// Armazenar no banco de dados
// INSERT INTO arquivos (nome_original, nome_armazenado, mime_type, usuario_id, data_upload)
// VALUES (?, ?, ?, ?, NOW())
Servir Arquivos com Segurança
Crie um script download.php que valide permissões:
<?php
session_start();
$arquivoId = $_GET['id'] ?? null;
if (!$arquivoId) die('ID inválido.');
// Buscar arquivo no banco e verificar permissão do usuário
$pdo = new PDO('mysql:host=localhost;dbname=app', 'user', 'pass');
$stmt = $pdo->prepare('SELECT nome_armazenado, mime_type FROM arquivos
WHERE id = ? AND usuario_id = ?');
$stmt->execute([$arquivoId, $_SESSION['usuario_id']]);
$arquivo = $stmt->fetch();
if (!$arquivo) die('Arquivo não encontrado.');
$caminhoArquivo = '/var/www/uploads/' . $arquivo['nome_armazenado'];
if (!file_exists($caminhoArquivo)) die('Arquivo não existe.');
header('Content-Type: ' . $arquivo['mime_type']);
header('Content-Disposition: attachment; filename="documento.pdf"');
header('Content-Length: ' . filesize($caminhoArquivo));
readfile($caminhoArquivo);
Defesas Adicionais
Verificar Erros de Upload
<?php
if ($arquivo['error'] !== UPLOAD_ERR_OK) {
$erros = [
UPLOAD_ERR_INI_SIZE => 'Arquivo excede limite ini_set',
UPLOAD_ERR_FORM_SIZE => 'Arquivo excede MAX_FILE_SIZE',
UPLOAD_ERR_PARTIAL => 'Upload interrompido',
UPLOAD_ERR_NO_FILE => 'Nenhum arquivo',
UPLOAD_ERR_NO_TMP_DIR => 'Diretório temporário ausente',
UPLOAD_ERR_CANT_WRITE => 'Erro ao escrever arquivo',
UPLOAD_ERR_EXTENSION => 'Upload bloqueado por extensão'
];
die('Erro: ' . ($erros[$arquivo['error']] ?? 'Desconhecido'));
}
Desabilitar Execução no Diretório de Uploads
Se usar Apache, crie .htaccess no diretório de uploads:
<FilesMatch "\.php$">
Deny from all
</FilesMatch>
AddType text/plain .php .phtml .php3 .php4 .php5 .php7 .phar
Para Nginx, configure no bloco location:
location /uploads {
location ~ \.php$ {
return 403;
}
}
Conclusão
Upload seguro exige defesa em profundidade. Resumo dos três pilares:
-
Validação rigorosa: Use
fileinfoegetimagesize(), nunca confie apenas em extensão ou MIME-Type do cliente. -
Armazenamento protegido: Coloque uploads fora da raiz web, renomeie arquivos com valores aleatórios e implemente controle de acesso no código.
-
Defesas periféricas: Configure PHP corretamente, desabilite execução de scripts no diretório de uploads e valide erros de upload.
A segurança é um processo contínuo. Mantenha seu conhecimento atualizado acompanhando os CVEs relacionados a upload de arquivos.