O upload de arquivos é uma funcionalidade comum em aplicações web, permitindo que usuários enviem imagens, documentos, vídeos e outros tipos de arquivo para o servidor. Em PHP, essa tarefa é facilitada pelas superglobais e funções específicas, mas exige cuidados especiais com validação e segurança para evitar vulnerabilidades. Nesta aula, vamos explorar os mecanismos de upload, desde a captura dos dados do formulário até o armazenamento seguro no servidor, passando pelas principais validações e riscos.

Dominar o upload de arquivos é essencial para construir sistemas robustos, como galerias de fotos, sistemas de anexos em e-mails, plataformas de compartilhamento de documentos, entre outros. Vamos começar entendendo como o PHP recebe os arquivos enviados pelo cliente.

$_FILES

A superglobal $_FILES é um array associativo que contém todos os dados dos arquivos enviados via método POST com o enctype="multipart/form-data". Cada campo de arquivo no formulário gera uma entrada em $_FILES, que por sua vez contém subarrays com informações como nome original, tipo MIME, tamanho em bytes, nome temporário no servidor e possíveis erros.

Por exemplo, se o formulário tiver um input com name="arquivo", $_FILES['arquivo'] será um array com as chaves: 'name', 'type', 'tmp_name', 'error' e 'size'. O arquivo é inicialmente armazenado em um diretório temporário no servidor, e o script PHP deve movê-lo para um local definitivo usando move_uploaded_file().

<?php
// 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>

<?php
// upload.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $file = $_FILES['arquivo'];
    echo 'Nome original: ' . $file['name'] . '<br>';
    echo 'Tipo MIME: ' . $file['type'] . '<br>';
    echo 'Tamanho: ' . $file['size'] . ' bytes<br>';
    echo 'Nome temporário: ' . $file['tmp_name'] . '<br>';
    echo 'Código de erro: ' . $file['error'] . '<br>';
}
?>

O código de erro (error) pode ser 0 (UPLOAD_ERR_OK), 1 (tamanho excede upload_max_filesize no php.ini), 2 (tamanho excede MAX_FILE_SIZE no formulário), 3 (upload parcial), 4 (nenhum arquivo enviado), 6 (falta pasta temporária), 7 (falha ao escrever no disco) ou 8 (extensão bloqueada).

move_uploaded_file

A função move_uploaded_file() é a maneira segura de mover um arquivo enviado de sua localização temporária para um destino permanente. Ela verifica se o arquivo foi realmente enviado via HTTP POST (evitando manipulação de caminhos) e, em caso afirmativo, move o arquivo. A sintaxe é: move_uploaded_file(string $filename, string $destination): bool.

O primeiro parâmetro é o caminho temporário ($_FILES['arquivo']['tmp_name']), e o segundo é o caminho de destino desejado. É importante definir um diretório de destino com permissões adequadas (geralmente 0755) e, idealmente, fora da raiz pública para evitar acesso direto.

<?php
$uploadDir = __DIR__ . '/uploads/';
$tmpName = $_FILES['arquivo']['tmp_name'];
$destino = $uploadDir . basename($_FILES['arquivo']['name']);

if (move_uploaded_file($tmpName, $destino)) {
    echo "Arquivo enviado com sucesso para: $destino";
} else {
    echo "Falha ao mover o arquivo.";
}
?>

Observações importantes: sempre use basename() para evitar path traversal (ex.: nomes como ../../../etc/passwd). Além disso, nunca confie no nome original do arquivo para salvá-lo; gere um nome único (ex.: uniqid() ou hash) para evitar sobrescrita e problemas de segurança.

Validação (tipo, tamanho)

A validação é crucial para garantir que apenas arquivos permitidos sejam aceitos. Dois aspectos principais são o tipo MIME e o tamanho do arquivo. O tipo MIME enviado pelo cliente ($_FILES['arquivo']['type']) não deve ser confiável, pois pode ser falsificado. Em vez disso, use funções como mime_content_type() ou finfo para determinar o tipo real do arquivo no servidor.

O tamanho pode ser verificado comparando $_FILES['arquivo']['size'] com um limite definido (em bytes). Além disso, o php.ini define limites globais (upload_max_filesize, post_max_size) que devem ser respeitados. Exemplo de validação completa:

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

$file = $_FILES['arquivo'];
$errors = [];

// Verifica se houve erro no upload
if ($file['error'] !== UPLOAD_ERR_OK) {
    $errors[] = "Erro no upload: código " . $file['error'];
}

// Verifica tamanho
if ($file['size'] > $maxSize) {
    $errors[] = "Arquivo muito grande. Máximo permitido: 2MB.";
}

// Verifica tipo real usando finfo
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$realType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);

if (!in_array($realType, $allowedTypes)) {
    $errors[] = "Tipo de arquivo não permitido: $realType";
}

if (empty($errors)) {
    $destino = __DIR__ . '/uploads/' . uniqid() . '_' . basename($file['name']);
    if (move_uploaded_file($file['tmp_name'], $destino)) {
        echo "Upload realizado com sucesso!";
    } else {
        echo "Falha ao salvar o arquivo.";
    }
} else {
    foreach ($errors as $error) {
        echo "<p>$error</p>";
    }
}
?>

Além do tipo e tamanho, é recomendável validar a extensão do arquivo (embora não substitua a verificação MIME) e, para imagens, usar getimagesize() para confirmar que é uma imagem válida.

Riscos de segurança

Upload de arquivos é uma das funcionalidades mais críticas em termos de segurança. Os principais riscos incluem: execução de código arbitrário (se o usuário enviar um script PHP e ele for acessível via web), path traversal (acesso a diretórios não autorizados), sobrecarga do servidor (arquivos muito grandes ou muitos arquivos), e injeção de conteúdo malicioso (como scripts em arquivos PDF ou imagens).

Para mitigar esses riscos, siga estas boas práticas: 1) Armazene os arquivos fora da raiz pública (ex.: /var/www/uploads/ em vez de /var/www/html/uploads/) e sirva-os via um script intermediário que leia e exiba o arquivo (ex.: download.php?id=123). 2) Nunca use o nome original do arquivo; gere nomes aleatórios com extensão segura (ex.: apenas extensões permitidas). 3) Valide o tipo MIME real no servidor, não confie no tipo enviado. 4) Defina limites de tamanho e quantidade de arquivos. 5) Desative a execução de scripts no diretório de uploads (via .htaccess ou configuração do servidor). 6) Utilize ferramentas de verificação de vírus se possível. 7) Implemente autenticação e autorização para uploads.

// Exemplo de .htaccess para desativar execução no diretório uploads
<IfModule mod_php7.c>
    php_flag engine off
</IfModule>
<IfModule mod_php8.c>
    php_flag engine off
</IfModule>
# Opcional: negar acesso a todos os arquivos
Order deny,allow
Deny from all
# Mas permitir acesso a arquivos específicos via script intermediário

Outra medida importante é verificar se o arquivo é realmente uma imagem (se for o caso) usando funções como getimagesize() ou exif_imagetype(), que também ajudam a evitar arquivos maliciosos disfarçados.

Boas práticas e observações finais

Sempre valide e sanitize os dados do upload. Utilize a função is_uploaded_file() para confirmar que o arquivo veio de um upload legítimo (embora move_uploaded_file já faça isso). Mantenha logs de uploads para auditoria. E lembre-se: a segurança é um processo contínuo; mantenha-se atualizado sobre novas vulnerabilidades e corrija configurações inadequadas.

Referências

Exercícios

  1. Crie um formulário HTML com um campo de upload de arquivo (nome="foto") e um script PHP que exiba o nome original e o tamanho do arquivo enviado.

    ✓ Resposta:
    <!-- formulario.html -->
    <form action="upload.php" method="post" enctype="multipart/form-data">
        <input type="file" name="foto">
        <input type="submit" value="Enviar">
    </form>
    
    <!-- upload.php -->
    <?php
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $file = $_FILES['foto'];
        echo 'Nome: ' . htmlspecialchars($file['name']) . '<br>';
        echo 'Tamanho: ' . $file['size'] . ' bytes';
    }
    ?>
  2. Implemente um script que mova um arquivo enviado para o diretório 'uploads/' com um nome único (use uniqid).

    ✓ Resposta:
    <?php
    $uploadDir = __DIR__ . '/uploads/';
    if (!is_dir($uploadDir)) {
        mkdir($uploadDir, 0755, true);
    }
    $tmpName = $_FILES['arquivo']['tmp_name'];
    $ext = pathinfo($_FILES['arquivo']['name'], PATHINFO_EXTENSION);
    $novoNome = uniqid() . '.' . $ext;
    $destino = $uploadDir . $novoNome;
    
    if (move_uploaded_file($tmpName, $destino)) {
        echo "Arquivo salvo como: $novoNome";
    } else {
        echo "Falha ao mover.";
    }
    ?>
  3. Faça a validação de um upload permitindo apenas imagens JPEG e PNG de até 1 MB, usando a função finfo para verificar o tipo real.

    ✓ Resposta:
    <?php
    $allowedTypes = ['image/jpeg', 'image/png'];
    $maxSize = 1 * 1024 * 1024; // 1 MB
    
    $file = $_FILES['arquivo'];
    $errors = [];
    
    if ($file['error'] !== UPLOAD_ERR_OK) {
        $errors[] = "Erro no upload.";
    }
    
    if ($file['size'] > $maxSize) {
        $errors[] = "Arquivo muito grande (máx 1MB).";
    }
    
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $realType = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);
    
    if (!in_array($realType, $allowedTypes)) {
        $errors[] = "Tipo não permitido: $realType";
    }
    
    if (empty($errors)) {
        $destino = __DIR__ . '/uploads/' . uniqid() . '_' . basename($file['name']);
        move_uploaded_file($file['tmp_name'], $destino);
        echo "Upload OK";
    } else {
        foreach ($errors as $e) echo "<p>$e</p>";
    }
    ?>
  4. Explique por que é perigoso confiar no tipo MIME enviado pelo cliente ($_FILES['arquivo']['type']) e como mitigar esse risco.

    ✓ Resposta:O tipo MIME enviado pelo cliente pode ser facilmente falsificado (por exemplo, um script PHP pode ser enviado com type image/jpeg). Para mitigar, deve-se usar funções do servidor como finfo_file() ou mime_content_type() para determinar o tipo real do arquivo. Além disso, nunca confie na extensão do arquivo; valide o conteúdo.
  5. Liste três medidas de segurança essenciais ao implementar upload de arquivos em produção.

    ✓ Resposta:1) Armazenar arquivos fora da raiz pública e servi-los via script intermediário. 2) Validar o tipo real do arquivo no servidor (finfo). 3) Gerar nomes únicos e seguros (uniqid) e desativar execução de scripts no diretório de uploads.