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

Exercícios

  1. Escreva um script PHP que valide o tipo real de um arquivo enviado via upload usando a função finfo e permita apenas arquivos PDF.

✓ Resposta:
$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.');
}
  • Explique por que armazenar arquivos fora da raiz web é importante e como você serviria esses arquivos ao usuário.
  • ✓ Resposta: Armazenar fora da raiz web impede acesso direto via URL, evitando execução de scripts maliciosos. Para servir os arquivos, crie um script intermediário que valide permissões e envie o arquivo com headers apropriados, como readfile().
  • Implemente o código para renomear um arquivo enviado usando um nome aleatório e movê-lo para o diretório de uploads.
  • ✓ Resposta:
    $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.');
    }
    
  • Liste os principais riscos de segurança em uploads de arquivos e descreva uma medida de mitigação para cada um.
  • ✓ Resposta: Riscos: execução de código (mitigação: armazenar fora da raiz e desativar execução), XSS (mitigação: servir com Content-Disposition attachment), consumo de recursos (mitigação: limitar tamanho), inclusão de arquivos (mitigação: renomear e validar), conteúdo impróprio (mitigação: revisão manual).
  • Crie um script PHP que verifique o erro de upload usando $_FILES['arquivo']['error'] e exiba uma mensagem adequada para cada código de erro.
  • ✓ Resposta:
    $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.';
    }