Uploads de arquivos são uma funcionalidade comum em aplicações web, mas também são uma das principais portas de entrada para ataques. Se não forem implementados corretamente, podem permitir que um atacante envie arquivos maliciosos, como scripts PHP, e execute-os no servidor. Nesta aula, vamos explorar como criar uploads seguros em PHP, focando em quatro pilares essenciais: validação do tipo real do arquivo, armazenamento fora da raiz web, renomeação de arquivos e a compreensão dos riscos envolvidos.

Vamos abordar desde a configuração do formulário HTML até o tratamento no servidor, com exemplos práticos que você pode adaptar para seus projetos. Ao final, você terá uma base sólida para implementar uploads seguros e evitar vulnerabilidades comuns.

Validação de tipo real

A validação do tipo de arquivo é crucial para evitar que um atacante envie um script disfarçado de imagem ou documento. O PHP fornece duas formas principais de verificar o tipo: a extensão do arquivo e o MIME type. No entanto, ambas podem ser falsificadas. Por isso, é necessário validar o conteúdo real do arquivo.

Uma abordagem robusta é usar a função finfo_file() da extensão Fileinfo, que analisa os bytes do arquivo e retorna o tipo MIME real. Isso impede que um arquivo com extensão .jpg, mas contendo código PHP, seja aceito como imagem. A seguir, um exemplo de como fazer essa validação:

<?php
// Supondo que o arquivo foi enviado via POST
$arquivo = $_FILES['arquivo'];

// Cria um objeto Fileinfo
$finfo = new finfo(FILEINFO_MIME_TYPE);

// Obtém o tipo MIME real do arquivo temporário
$mimeReal = $finfo->file($arquivo['tmp_name']);

// Lista de tipos MIME permitidos
$tiposPermitidos = ['image/jpeg', 'image/png', 'application/pdf'];

if (!in_array($mimeReal, $tiposPermitidos)) {
    die('Tipo de arquivo não permitido.');
}

// Continue o processamento...
?>

Além do MIME type, é importante verificar a extensão, mas não confiar apenas nela. Você pode combinar a verificação da extensão com o MIME real para uma defesa em camadas. Lembre-se de que a constante FILEINFO_MIME_TYPE retorna apenas o tipo, sem o charset. Para uma validação mais completa, você pode usar FILEINFO_MIME, mas o primeiro é suficiente para a maioria dos casos.

Armazenamento fora da raiz web

Se você armazenar arquivos enviados dentro do diretório público do seu site (como public/ ou htdocs/), eles podem ser acessados diretamente pela URL. Isso é perigoso, pois se um arquivo malicioso for enviado, ele pode ser executado ou baixado. A solução é armazenar os uploads em um diretório fora da raiz web, que não seja acessível diretamente pelo navegador.

Por exemplo, se seu site está em /var/www/html, você pode criar um diretório /var/www/uploads e configurar o PHP para salvar os arquivos lá. O acesso a esses arquivos deve ser feito através de um script PHP intermediário que verifica permissões e serve o arquivo de forma controlada. Veja um exemplo:

<?php
// Diretório de uploads fora da raiz web
$diretorioUpload = '/var/www/uploads/';

// Move o arquivo temporário para o diretório de uploads
if (move_uploaded_file($arquivo['tmp_name'], $diretorioUpload . $nomeArquivo)) {
    echo 'Upload realizado com sucesso!';
} else {
    echo 'Falha no upload.';
}
?>

Para servir esses arquivos, crie um script como download.php?id=123, que busca o arquivo no diretório, verifica se o usuário tem permissão e envia o conteúdo com os headers adequados. Isso garante que o arquivo não seja executado como script, pois o PHP não o interpretará (a menos que você explicitamente inclua).

Renomeação

Renomear os arquivos enviados é uma prática de segurança importante. O nome original fornecido pelo usuário pode conter caracteres perigosos ou ser usado para sobrescrever arquivos existentes. Além disso, nomes previsíveis facilitam ataques. A melhor abordagem é gerar um nome aleatório, como um hash ou um UUID, e preservar a extensão original (se for permitida).

Exemplo de renomeação usando uniqid() e a extensão original:

<?php
// Obtém a extensão do arquivo original
$extensao = pathinfo($arquivo['name'], PATHINFO_EXTENSION);

// Gera um nome único
$novoNome = uniqid('upload_', true) . '.' . $extensao;

// Move o arquivo com o novo nome
if (move_uploaded_file($arquivo['tmp_name'], $diretorioUpload . $novoNome)) {
    // Salve o novo nome no banco de dados
}
?>

Para maior segurança, você pode usar random_bytes() e bin2hex() para gerar um nome criptograficamente aleatório:

$novoNome = bin2hex(random_bytes(16)) . '.' . $extensao;

Isso torna o nome praticamente impossível de adivinhar. Além disso, ao renomear, você evita problemas com caminhos de diretórios (path traversal) e garante que o arquivo seja armazenado com um nome seguro.

Riscos

Os uploads de arquivos apresentam vários riscos que devem ser mitigados. O mais crítico é a execução de código arbitrário: se um atacante consegue enviar um arquivo PHP e ele for armazenado na raiz web, ele pode ser executado ao acessar a URL. Outros riscos incluem:

  • Inclusão de arquivos (LFI/RFI): Se o aplicativo inclui arquivos com base no nome do upload, um atacante pode explorar.
  • DoS (Denial of Service): Arquivos muito grandes podem esgotar o espaço em disco ou a memória.
  • Malware: Arquivos infectados podem ser baixados por outros usuários.
  • Poluição de conteúdo: Arquivos ofensivos ou ilegais podem ser armazenados.

Para mitigar esses riscos, adote as seguintes medidas:

  • Limite o tamanho do arquivo no PHP (upload_max_filesize) e no formulário.
  • Valide o tipo real do arquivo (como visto acima).
  • Armazene fora da raiz web.
  • Renomeie os arquivos.
  • Use um script de download para servir os arquivos, evitando que sejam executados.
  • Considere usar um serviço de armazenamento externo (como S3) para isolar ainda mais.

Boas práticas e observações finais

Além das técnicas abordadas, é fundamental configurar corretamente o PHP e o servidor. No php.ini, ajuste upload_max_filesize, post_max_size e max_file_uploads conforme a necessidade. No servidor web (Apache ou Nginx), configure permissões adequadas para o diretório de uploads (por exemplo, 755 para diretórios e 644 para arquivos) e desabilite a execução de scripts no diretório de uploads, se possível.

Uma camada adicional é usar um anti-vírus ou serviços de análise de conteúdo, como o ClamAV, para verificar arquivos antes de armazená-los definitivamente. Em aplicações críticas, considere a conversão de imagens (redimensionamento, recompressão) para neutralizar possíveis payloads maliciosos.

Lembre-se de que a segurança é um processo contínuo. Sempre mantenha o PHP e as bibliotecas atualizadas, e revise regularmente o código de upload para novas vulnerabilidades.

Referências

Exercícios

  1. Exercício 1: Implemente um script PHP que valide o tipo real de um arquivo enviado usando finfo_file() e permita apenas imagens JPEG e PNG. Exiba uma mensagem de erro caso o tipo não seja permitido.

    ✓ Resposta:
    <?php
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $arquivo = $_FILES['arquivo'];
        $finfo = new finfo(FILEINFO_MIME_TYPE);
        $mimeReal = $finfo->file($arquivo['tmp_name']);
        $permitidos = ['image/jpeg', 'image/png'];
        if (!in_array($mimeReal, $permitidos)) {
            echo 'Tipo não permitido.';
        } else {
            echo 'Arquivo válido.';
        }
    }
    ?>
  2. Exercício 2: Crie um script que mova o arquivo enviado para um diretório localizado fora da raiz web (ex.: /var/www/uploads) e exiba uma mensagem de sucesso ou falha.

    ✓ Resposta:
    <?php
    $diretorioUpload = '/var/www/uploads/';
    if (move_uploaded_file($_FILES['arquivo']['tmp_name'], $diretorioUpload . $_FILES['arquivo']['name'])) {
        echo 'Upload realizado com sucesso!';
    } else {
        echo 'Falha no upload.';
    }
    ?>
  3. Exercício 3: Escreva uma função que receba o array $_FILES['arquivo'] e retorne um nome seguro (aleatório) com a extensão original. Use random_bytes().

    ✓ Resposta:
    function gerarNomeSeguro($arquivo) {
        $extensao = pathinfo($arquivo['name'], PATHINFO_EXTENSION);
        $nome = bin2hex(random_bytes(16));
        return $nome . '.' . $extensao;
    }
    ?>
  4. Exercício 4: Liste três riscos associados a uploads de arquivos e explique como cada um pode ser mitigado.

    ✓ Resposta: 1. Execução de código arbitrário: Mitigação: armazenar fora da raiz web, validar tipo real, renomear. 2. DoS: Mitigação: limitar tamanho do arquivo e usar limites no PHP. 3. Malware: Mitigação: usar antivírus e validações de conteúdo.
  5. Exercício 5: Crie um script de download que sirva arquivos de um diretório fora da raiz web, verificando se o arquivo existe e enviando os headers corretos.

    ✓ Resposta:
    <?php
    $arquivo = '/var/www/uploads/' . $_GET['arquivo'];
    if (file_exists($arquivo)) {
        header('Content-Description: File Transfer');
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="' . basename($arquivo) . '"');
        readfile($arquivo);
        exit;
    } else {
        http_response_code(404);
        echo 'Arquivo não encontrado.';
    }
    ?>