Uploads seguros
Esta aula aborda como implementar uploads de arquivos seguros em PHP, cobrindo validação de tipo real, armazenamento fora da raiz web, renomeação de arquivos e riscos comuns. Você aprenderá a proteger sua aplicação contra ataques como upload de scripts maliciosos e a aplicar boas práticas de segurança.
O upload de arquivos é uma funcionalidade comum em aplicações web, mas também é uma das principais portas de entrada para ataques. Nesta aula, você aprenderá a implementar uploads seguros em PHP, cobrindo desde a validação do tipo real do arquivo até o armazenamento adequado e a renomeação. Vamos explorar os riscos envolvidos e as melhores práticas para mitigá-los, garantindo que sua aplicação não seja vulnerável a uploads maliciosos.
Vamos começar entendendo que a segurança em uploads não se resume a verificar a extensão do arquivo ou o tipo MIME informado pelo cliente. É essencial validar o conteúdo real do arquivo, armazená-lo fora do diretório público e usar nomes aleatórios para evitar a execução de scripts. Ao final, você terá um guia completo para implementar uploads robustos e seguros.
Validação de tipo real
A primeira linha de defesa em uploads é validar o tipo do arquivo. No entanto, confiar apenas na extensão ou no tipo MIME enviado pelo navegador é arriscado, pois esses dados podem ser facilmente manipulados. Por isso, a validação deve ser feita com base no conteúdo real do arquivo, utilizando funções como finfo para detectar o tipo MIME verdadeiro, e até mesmo verificações adicionais para formatos específicos.
Por exemplo, se você permite apenas imagens, pode usar a função getimagesize() para verificar se o arquivo é uma imagem válida. Essa função retorna informações sobre a imagem ou false se o arquivo não for uma imagem. Isso é mais seguro do que apenas olhar a extensão.
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['arquivo']['tmp_name']);
finfo_close($finfo);
$permitidos = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mime, $permitidos)) {
die('Tipo de arquivo não permitido.');
}
// Para imagens, validação adicional com getimagesize
$info = @getimagesize($_FILES['arquivo']['tmp_name']);
if ($info === false) {
die('Arquivo não é uma imagem válida.');
}
Além disso, é importante limitar o tamanho do arquivo e verificar se o upload foi bem-sucedido usando $_FILES['arquivo']['error']. Isso evita processar arquivos que não foram totalmente carregados ou que excedem o limite definido.
Armazenamento fora da raiz web
Armazenar arquivos enviados pelo usuário dentro do diretório público da aplicação (como public/ ou htdocs) é um erro grave. Se um atacante conseguir enviar um script PHP disfarçado, ele pode ser executado diretamente pelo servidor, comprometendo todo o sistema. A solução é armazenar os arquivos em um diretório privado, fora da raiz web, e servir esses arquivos através de um script intermediário que controla o acesso.
Por exemplo, em uma estrutura de projeto comum, você pode ter:
/projeto
/public
index.php
uploads.php
/uploads
(arquivos enviados)
O diretório /uploads não é acessível diretamente pelo navegador. Para exibir os arquivos, você cria um script em public/uploads.php que recebe um identificador e serve o arquivo com os headers corretos, garantindo que apenas usuários autorizados possam acessá-lo.
// public/uploads.php
$arquivoId = $_GET['id'];
$caminho = __DIR__ . '/../uploads/' . $arquivoId;
if (!file_exists($caminho)) {
http_response_code(404);
exit('Arquivo não encontrado.');
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $caminho);
finfo_close($finfo);
header('Content-Type: ' . $mime);
header('Content-Length: ' . filesize($caminho));
readfile($caminho);
Essa abordagem também permite aplicar autenticação e autorização antes de servir o arquivo, além de evitar que scripts sejam executados, pois o conteúdo é enviado como download ou renderizado conforme o tipo MIME.
Renomeação
Quando um arquivo é enviado, o nome original pode conter caracteres perigosos, como barras ou pontos, ou ser um nome de script (por exemplo, shell.php). Para evitar isso, é essencial renomear o arquivo para um nome gerado aleatoriamente, mantendo a extensão segura (ou sem extensão, se possível). Isso impede que o nome original cause problemas e dificulta ataques.
Uma prática comum é usar uniqid() ou bin2hex(random_bytes()) para gerar um nome único. Você pode também incluir a extensão, mas apenas se ela for permitida e corresponder ao tipo real. Exemplo:
$extensao = pathinfo($_FILES['arquivo']['name'], PATHINFO_EXTENSION);
$extensao = strtolower($extensao);
// Lista de extensões permitidas
$permitidas = ['jpg', 'jpeg', 'png', 'gif'];
if (!in_array($extensao, $permitidas)) {
die('Extensão não permitida.');
}
$novoNome = bin2hex(random_bytes(16)) . '.' . $extensao;
$destino = __DIR__ . '/../uploads/' . $novoNome;
if (!move_uploaded_file($_FILES['arquivo']['tmp_name'], $destino)) {
die('Falha ao mover o arquivo.');
}
Além de renomear, é importante garantir que o diretório de destino tenha permissões restritas (por exemplo, 0755) e que o arquivo não seja executável. Se possível, desative a execução de scripts no diretório de uploads via configuração do servidor (como .htaccess ou nginx.conf).
Riscos
Os uploads inseguros podem levar a vários tipos de ataque. O mais crítico é a execução de código arbitrário: se um atacante enviar um arquivo PHP disfarçado e ele for armazenado em um diretório público, ele pode ser executado ao ser acessado, dando ao atacante controle total do servidor. Outros riscos incluem:
- Cross-site scripting (XSS): Se o arquivo for servido com um tipo MIME incorreto ou sem headers de segurança, um script JavaScript pode ser executado no navegador de outros usuários.
- Consumo de recursos: Arquivos muito grandes podem sobrecarregar o servidor, causando negação de serviço.
- Inclusão de arquivos: Se o nome do arquivo for usado em operações de inclusão (como
include), um atacante pode manipular o caminho. - Poluição de conteúdo: Upload de conteúdo ilegal ou impróprio pode causar problemas legais.
Para mitigar esses riscos, é fundamental combinar todas as técnicas abordadas: validação de tipo real, armazenamento fora da raiz, renomeação, e também definir limites de tamanho, verificar erros de upload e usar headers de segurança ao servir arquivos, como Content-Disposition: attachment para forçar download.
Boas práticas e observações finais
Além das técnicas apresentadas, considere as seguintes boas práticas:
- Use HTTPS para proteger o tráfego durante o upload.
- Valide também o tamanho do arquivo no servidor, não apenas no cliente.
- Mantenha o diretório de uploads sem permissão de execução de scripts.
- Registre logs de uploads para auditoria.
- Considere usar serviços de armazenamento em nuvem (como S3) para isolar ainda mais os arquivos.
Lembre-se de que a segurança é um processo contínuo. Sempre mantenha o PHP e as bibliotecas atualizadas e revise periodicamente suas implementações de upload.
Referências
- PHP Manual - Handling File Uploads
- PHP Manual - finfo_file
- PHP Manual - getimagesize
- OWASP - Unrestricted File Upload
- MDN - Content-Disposition
- PHP Manual - move_uploaded_file
Exercícios
- Escreva um script PHP que valide o tipo real de um arquivo enviado via upload usando a função
finfoe permita apenas arquivos PDF.
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['arquivo']['tmp_name']);
finfo_close($finfo);
if ($mime !== 'application/pdf') {
die('Somente arquivos PDF são permitidos.');
}
readfile().$ext = pathinfo($_FILES['arquivo']['name'], PATHINFO_EXTENSION);
$novoNome = bin2hex(random_bytes(16)) . '.' . $ext;
$destino = __DIR__ . '/uploads/' . $novoNome;
if (!move_uploaded_file($_FILES['arquivo']['tmp_name'], $destino)) {
die('Falha ao mover.');
}
$_FILES['arquivo']['error'] e exiba uma mensagem adequada para cada código de erro.$erro = $_FILES['arquivo']['error'];
switch ($erro) {
case UPLOAD_ERR_OK:
echo 'Upload ok.';
break;
case UPLOAD_ERR_INI_SIZE:
echo 'Arquivo excede o tamanho máximo definido no PHP.';
break;
case UPLOAD_ERR_FORM_SIZE:
echo 'Arquivo excede o tamanho máximo definido no formulário.';
break;
case UPLOAD_ERR_PARTIAL:
echo 'Upload parcial.';
break;
case UPLOAD_ERR_NO_FILE:
echo 'Nenhum arquivo enviado.';
break;
default:
echo 'Erro desconhecido.';
}