Upload de Arquivos com PHP: Validação e Segurança na Prática Já leu

Upload de Arquivos com PHP: Validação e Segurança Upload de arquivos é uma funcionalidade comum em aplicações web, mas também representa um dos maiores vetores de ataque se implementado incorretamente. Neste artigo, abordaremos desde o formulário HTML até técnicas avançadas de validação, focando sempre em segurança. Vou compartilhar a experiência que acumulei em anos trabalhando com essa feature crítica. Fundamentos: HTML e Configuração do PHP O Formulário HTML Correto O formulário deve usar para transmitir arquivos corretamente. Sem isso, o servidor não recebe os dados do upload. O atributo é apenas uma validação no cliente — nunca confie nela. O é processado pelo PHP, mas também adicione validação no servidor. Configurações PHP Essenciais Certifique-se de que seu está configurado corretamente: Mantenha em uma partição separada e com permissões restritas (755 ou 750). Nunca deixe como padrão do sistema. Validação Segura no Servidor Validação de Tipo de Arquivo A validação mais comum — verificar a extensão — é frágil e perigosa.

Upload de Arquivos com PHP: Validação e Segurança

Upload de arquivos é uma funcionalidade comum em aplicações web, mas também representa um dos maiores vetores de ataque se implementado incorretamente. Neste artigo, abordaremos desde o formulário HTML até técnicas avançadas de validação, focando sempre em segurança. Vou compartilhar a experiência que acumulei em anos trabalhando com essa feature crítica.

Fundamentos: HTML e Configuração do PHP

O Formulário HTML Correto

O formulário deve usar enctype="multipart/form-data" para transmitir arquivos corretamente. Sem isso, o servidor não recebe os dados do upload.

<form method="POST" enctype="multipart/form-data">
    <input type="file" name="arquivo" accept=".pdf,.jpg,.png" required>
    <input type="hidden" name="MAX_FILE_SIZE" value="5242880">
    <button type="submit">Enviar</button>
</form>

O atributo accept é apenas uma validação no cliente — nunca confie nela. O MAX_FILE_SIZE é processado pelo PHP, mas também adicione validação no servidor.

Configurações PHP Essenciais

Certifique-se de que seu php.ini está configurado corretamente:

upload_max_filesize = 10M
post_max_size = 10M
upload_tmp_dir = /tmp/php_uploads
file_uploads = On

Mantenha upload_tmp_dir em uma partição separada e com permissões restritas (755 ou 750). Nunca deixe como padrão do sistema.

Validação Segura no Servidor

Validação de Tipo de Arquivo

A validação mais comum — verificar a extensão — é frágil e perigosa. Um usuário mal-intencionado pode renomear um .php para .jpg e contornar essa verificação. Use a função mime_content_type() ou melhor ainda, a extensão fileinfo:

<?php
$arquivo = $_FILES['arquivo'] ?? null;

if (!$arquivo) {
    die('Nenhum arquivo enviado.');
}

// Validar tamanho
$maxSize = 5 * 1024 * 1024; // 5MB
if ($arquivo['size'] > $maxSize) {
    die('Arquivo muito grande.');
}

// Validar tipo MIME com fileinfo
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $arquivo['tmp_name']);
finfo_close($finfo);

$mimePermitidos = ['image/jpeg', 'image/png', 'application/pdf'];
if (!in_array($mimeType, $mimePermitidos, true)) {
    die('Tipo de arquivo não permitido.');
}

Validação de Conteúdo

Para imagens, use getimagesize() para garantir que é realmente uma imagem válida:

<?php
if ($mimeType === 'image/jpeg' || $mimeType === 'image/png') {
    $imageInfo = @getimagesize($arquivo['tmp_name']);
    if ($imageInfo === false) {
        die('Imagem inválida ou corrompida.');
    }

    if ($imageInfo[0] > 4000 || $imageInfo[1] > 4000) {
        die('Dimensões da imagem excedem o limite.');
    }
}

Para PDFs, valide o cabeçalho do arquivo:

<?php
if ($mimeType === 'application/pdf') {
    $handle = fopen($arquivo['tmp_name'], 'rb');
    $header = fread($handle, 4);
    fclose($handle);

    if ($header !== "%PDF") {
        die('Arquivo PDF inválido.');
    }
}

Armazenamento Seguro

Diretório Protegido Fora da Raiz Web

Nunca armazene uploads na raiz pública (/public_html). Crie um diretório fora dela:

/var/www/
├── html/          (raiz web pública)
├── uploads/       (armazenamento seguro - NÃO acessível via HTTP)
└── ...

Configure as permissões:

mkdir -p /var/www/uploads
chmod 750 /var/www/uploads
chown www-data:www-data /var/www/uploads

Renomeação Segura do Arquivo

Nunca use o nome original do arquivo. Gere um nome único e seguro:

<?php
$nomeOriginal = basename($arquivo['name']);
$extensao = strtolower(pathinfo($nomeOriginal, PATHINFO_EXTENSION));

// Gerar nome único
$novoNome = bin2hex(random_bytes(16)) . '.' . $extensao;
$caminhoDestino = '/var/www/uploads/' . $novoNome;

if (!move_uploaded_file($arquivo['tmp_name'], $caminhoDestino)) {
    die('Erro ao mover arquivo.');
}

// Armazenar no banco de dados
// INSERT INTO arquivos (nome_original, nome_armazenado, mime_type, usuario_id, data_upload)
// VALUES (?, ?, ?, ?, NOW())

Servir Arquivos com Segurança

Crie um script download.php que valide permissões:

<?php
session_start();

$arquivoId = $_GET['id'] ?? null;
if (!$arquivoId) die('ID inválido.');

// Buscar arquivo no banco e verificar permissão do usuário
$pdo = new PDO('mysql:host=localhost;dbname=app', 'user', 'pass');
$stmt = $pdo->prepare('SELECT nome_armazenado, mime_type FROM arquivos 
                       WHERE id = ? AND usuario_id = ?');
$stmt->execute([$arquivoId, $_SESSION['usuario_id']]);
$arquivo = $stmt->fetch();

if (!$arquivo) die('Arquivo não encontrado.');

$caminhoArquivo = '/var/www/uploads/' . $arquivo['nome_armazenado'];

if (!file_exists($caminhoArquivo)) die('Arquivo não existe.');

header('Content-Type: ' . $arquivo['mime_type']);
header('Content-Disposition: attachment; filename="documento.pdf"');
header('Content-Length: ' . filesize($caminhoArquivo));

readfile($caminhoArquivo);

Defesas Adicionais

Verificar Erros de Upload

<?php
if ($arquivo['error'] !== UPLOAD_ERR_OK) {
    $erros = [
        UPLOAD_ERR_INI_SIZE => 'Arquivo excede limite ini_set',
        UPLOAD_ERR_FORM_SIZE => 'Arquivo excede MAX_FILE_SIZE',
        UPLOAD_ERR_PARTIAL => 'Upload interrompido',
        UPLOAD_ERR_NO_FILE => 'Nenhum arquivo',
        UPLOAD_ERR_NO_TMP_DIR => 'Diretório temporário ausente',
        UPLOAD_ERR_CANT_WRITE => 'Erro ao escrever arquivo',
        UPLOAD_ERR_EXTENSION => 'Upload bloqueado por extensão'
    ];

    die('Erro: ' . ($erros[$arquivo['error']] ?? 'Desconhecido'));
}

Desabilitar Execução no Diretório de Uploads

Se usar Apache, crie .htaccess no diretório de uploads:

<FilesMatch "\.php$">
    Deny from all
</FilesMatch>

AddType text/plain .php .phtml .php3 .php4 .php5 .php7 .phar

Para Nginx, configure no bloco location:

location /uploads {
    location ~ \.php$ {
        return 403;
    }
}

Conclusão

Upload seguro exige defesa em profundidade. Resumo dos três pilares:

  1. Validação rigorosa: Use fileinfo e getimagesize(), nunca confie apenas em extensão ou MIME-Type do cliente.

  2. Armazenamento protegido: Coloque uploads fora da raiz web, renomeie arquivos com valores aleatórios e implemente controle de acesso no código.

  3. Defesas periféricas: Configure PHP corretamente, desabilite execução de scripts no diretório de uploads e valide erros de upload.

A segurança é um processo contínuo. Mantenha seu conhecimento atualizado acompanhando os CVEs relacionados a upload de arquivos.

Referências


Artigos relacionados