O upload de arquivos é uma funcionalidade comum em aplicações web, permitindo que usuários enviem imagens, documentos ou outros arquivos para o servidor. Em PHP, o processo é gerenciado pelo array superglobal $_FILES e pela função move_uploaded_file(). Nesta aula, você aprenderá a implementar um sistema de upload seguro, validando tipo e tamanho dos arquivos, e compreendendo os riscos de segurança envolvidos.

Antes de começar, certifique-se de que seu formulário HTML tenha o atributo enctype="multipart/form-data" e um input do tipo file. O PHP então disponibilizará os dados do arquivo enviado no array $_FILES.

$_FILES

O array $_FILES é uma superglobal multidimensional que contém informações sobre todos os arquivos enviados via método POST. Cada input file no formulário gera uma entrada em $_FILES com o nome do campo. Dentro de cada entrada, há cinco índices principais:

  • name: O nome original do arquivo no cliente.
  • type: O tipo MIME do arquivo (ex: image/jpeg).
  • tmp_name: O caminho temporário do arquivo no servidor.
  • error: Código de erro (0 se não houve erro).
  • size: O tamanho do arquivo em bytes.

Exemplo de formulário HTML:

<form action="upload.php" method="post" enctype="multipart/form-data">
    <input type="file" name="arquivo">
    <input type="submit" value="Enviar">
</form>

No PHP, acessamos os dados assim:

$arquivo = $_FILES['arquivo'];
echo $arquivo['name']; // Nome original
echo $arquivo['type']; // Tipo MIME
echo $arquivo['tmp_name']; // Caminho temporário
echo $arquivo['error']; // Código de erro
echo $arquivo['size']; // Tamanho em bytes

move_uploaded_file

A função move_uploaded_file() move o arquivo enviado da localização temporária para um destino permanente no servidor. Ela recebe dois argumentos: o caminho do arquivo temporário (tmp_name) e o caminho de destino (incluindo nome do arquivo). Retorna true em sucesso, false em falha.

$destino = 'uploads/' . $_FILES['arquivo']['name'];
if (move_uploaded_file($_FILES['arquivo']['tmp_name'], $destino)) {
    echo 'Arquivo enviado com sucesso!';
} else {
    echo 'Erro ao mover o arquivo.';
}

É importante usar move_uploaded_file() em vez de copy() ou rename() porque ela verifica se o arquivo foi realmente enviado via upload HTTP, evitando manipulação maliciosa de caminhos temporários.

Validação (tipo, tamanho)

A validação é essencial para garantir que apenas arquivos permitidos sejam aceitos. Você deve verificar:

  • Tipo MIME: Use $_FILES['arquivo']['type'] ou a função mime_content_type() para verificar o tipo real do arquivo. Nunca confie apenas na extensão.
  • Tamanho: Compare $_FILES['arquivo']['size'] com um limite definido (ex: 2MB = 2 * 1024 * 1024).
  • Erro: Verifique o código de erro (0 = UPLOAD_ERR_OK).

Exemplo de validação:

$allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
$maxSize = 2 * 1024 * 1024; // 2MB

if ($_FILES['arquivo']['error'] !== UPLOAD_ERR_OK) {
    die('Erro no upload.');
}
if ($_FILES['arquivo']['size'] > $maxSize) {
    die('Arquivo muito grande. Máximo: 2MB.');
}
if (!in_array($_FILES['arquivo']['type'], $allowedTypes)) {
    die('Tipo de arquivo não permitido.');
}

Riscos de segurança

Upload de arquivos apresenta vários riscos de segurança se não for tratado corretamente:

  • Execução de código malicioso: Se o servidor permitir execução de scripts (PHP, Python, etc.) na pasta de upload, um invasor pode enviar um arquivo .php e executá-lo. Solução: armazenar arquivos fora da raiz web ou configurar o servidor para não executar scripts na pasta de upload.
  • Injeção de conteúdo: Arquivos com nomes maliciosos (ex: ../config.php) podem sobrescrever arquivos do sistema. Use basename() para limpar o nome e gere nomes únicos com uniqid() ou md5().
  • Negacão de serviço (DoS): Upload de arquivos muito grandes pode esgotar o espaço em disco. Limite o tamanho no PHP (upload_max_filesize, post_max_size) e no servidor.
  • Ataques de inclusão local de arquivo (LFI): Se o nome do arquivo for usado em funções como include(), pode permitir execução de código arbitrário. Nunca inclua arquivos de upload diretamente.

Boas práticas:

  • Armazene arquivos em um diretório fora da raiz pública.
  • Renomeie os arquivos com nomes aleatórios (ex: uniqid() . '.' . $extensao).
  • Valide o tipo MIME real usando finfo ou mime_content_type().
  • Verifique se o arquivo é uma imagem válida com getimagesize().

Referências

Exercícios

  1. Crie um formulário HTML que permita o upload de uma imagem (jpg, png) de até 1MB. Escreva o código PHP para processar o upload, validando tipo e tamanho, e movendo o arquivo para a pasta 'uploads/' com um nome único.
  2. ✓ Resposta:
    <!-- formulario.html -->
    <form action="upload.php" method="post" enctype="multipart/form-data">
        <input type="file" name="imagem" accept="image/jpeg,image/png">
        <input type="submit" value="Enviar">
    </form>
    
    <?php // upload.php
    $allowedTypes = ['image/jpeg', 'image/png'];
    $maxSize = 1 * 1024 * 1024; // 1MB
    
    if ($_FILES['imagem']['error'] !== UPLOAD_ERR_OK) {
        die('Erro no upload.');
    }
    if ($_FILES['imagem']['size'] > $maxSize) {
        die('Arquivo muito grande. Máximo 1MB.');
    }
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mimeType = finfo_file($finfo, $_FILES['imagem']['tmp_name']);
    finfo_close($finfo);
    if (!in_array($mimeType, $allowedTypes)) {
        die('Tipo de arquivo não permitido.');
    }
    $extensao = pathinfo($_FILES['imagem']['name'], PATHINFO_EXTENSION);
    $nomeUnico = uniqid() . '.' . $extensao;
    $destino = 'uploads/' . $nomeUnico;
    if (move_uploaded_file($_FILES['imagem']['tmp_name'], $destino)) {
        echo 'Upload realizado com sucesso: ' . $destino;
    } else {
        echo 'Erro ao mover o arquivo.';
    }
    ?>
  3. Explique por que é perigoso usar o nome original do arquivo (ex: $_FILES['arquivo']['name']) diretamente como destino em move_uploaded_file(). Dê um exemplo de ataque.
  4. ✓ Resposta:

    Usar o nome original permite que um invasor manipule o nome para incluir caminhos relativos (ex: ../config.php) e sobrescrever arquivos importantes do sistema. Por exemplo, se o destino for uploads/ e o nome for ../../config.php, o arquivo será salvo fora da pasta uploads, podendo sobrescrever um arquivo de configuração. Além disso, nomes como shell.php podem ser executados pelo servidor se a pasta permitir execução de scripts.

  5. Escreva uma função PHP que valide se um arquivo é uma imagem (JPEG ou PNG) usando a extensão finfo. A função deve receber o caminho do arquivo temporário e retornar true se for imagem, false caso contrário.
  6. ✓ Resposta:
    function isImage($tmpPath) {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime = finfo_file($finfo, $tmpPath);
        finfo_close($finfo);
        $allowed = ['image/jpeg', 'image/png'];
        return in_array($mime, $allowed);
    }
  7. Qual é a diferença entre $_FILES['arquivo']['type'] e o tipo MIME obtido por finfo_file()? Por que o segundo é mais seguro?
  8. ✓ Resposta:

    $_FILES['arquivo']['type'] é fornecido pelo cliente e pode ser facilmente falsificado (ex: um invasor pode enviar um arquivo PHP com type image/jpeg). Já finfo_file() examina o conteúdo real do arquivo para determinar o tipo MIME, sendo mais confiável. Portanto, para segurança, use finfo_file().

  9. Implemente um sistema de upload que rejeite arquivos com extensão .php, .phtml, .php5 e que também verifique o tipo MIME real para garantir que não sejam scripts disfarçados. Use pathinfo() e finfo.
  10. ✓ Resposta:
    $blockedExts = ['php', 'phtml', 'php5', 'php7', 'php8', 'shtml', 'cgi'];
    $ext = strtolower(pathinfo($_FILES['arquivo']['name'], PATHINFO_EXTENSION));
    if (in_array($ext, $blockedExts)) {
        die('Extensão não permitida.');
    }
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime = finfo_file($finfo, $_FILES['arquivo']['tmp_name']);
    finfo_close($finfo);
    $allowedMimes = ['image/jpeg', 'image/png', 'application/pdf'];
    if (!in_array($mime, $allowedMimes)) {
        die('Tipo de arquivo não permitido.');
    }
    // Proceder com move_uploaded_file...