Como fazer upload de arquivos em PHP de forma segura (passo a passo moderno)

Fazer upload de arquivos em PHP parece simples:

  • Cria um formulário
  • Usa move_uploaded_file()
  • E pronto…

Na prática, é aí que começam os problemas:

  • gente subindo arquivos gigantes
  • formatos que você não quer aceitar
  • nomes estranhos gerando conflito
  • até risco de segurança se o upload for mal feito

Neste guia, vou te mostrar como fazer upload em PHP moderno, com:

  • validação de tamanho
  • validação de tipo
  • nomes de arquivo seguros
  • estrutura organizada

1. Estrutura base do projeto

Você pode usar uma estrutura simples assim:

meu_upload/
  public/
    index.php          (formulário de upload)
    processa_upload.php (tratamento do upload)
  uploads/
    (aqui vão os arquivos enviados)

A pasta uploads/ precisa ter permissão de escrita pelo servidor (no Linux, normalmente 755 ou 775, dependendo do ambiente).


2. Formulário de upload (HTML)

Arquivo: public/index.php

<?php
// Opcional: mostrar mensagem de sucesso/erro via GET
$mensagem = $_GET['msg'] ?? '';
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <title>Upload de arquivo em PHP</title>
</head>
<body>
    <h1>Upload de arquivo</h1>

    <?php if ($mensagem !== ''): ?>
        <p><strong><?= htmlspecialchars($mensagem, ENT_QUOTES, 'UTF-8'); ?></strong></p>
    <?php endif; ?>

    <form action="processa_upload.php" method="post" enctype="multipart/form-data">
        <label for="arquivo">Escolha um arquivo (imagem JPG/PNG, até 2MB):</label><br><br>
        <input type="file" name="arquivo" id="arquivo" required>
        <br><br>
        <button type="submit">Enviar</button>
    </form>
</body>
</html>

Pontos importantes:

  • enctype="multipart/form-data" é obrigatório para upload funcionar.
  • Estamos limitando por texto a JPG/PNG até 2MB (vamos validar isso de verdade no PHP).

3. Tratando o upload com segurança

Arquivo: public/processa_upload.php

<?php

declare(strict_types=1);

// Pasta onde os arquivos serão salvos (fora de public seria ainda melhor)
$diretorioUploads = __DIR__ . '/../uploads';

// Cria a pasta se não existir
if (!is_dir($diretorioUploads)) {
    mkdir($diretorioUploads, 0775, true);
}

// Verifica se o arquivo foi enviado
if (!isset($_FILES['arquivo'])) {
    redirecionarComMensagem('Nenhum arquivo enviado.');
}

$arquivo = $_FILES['arquivo'];

// Verifica se houve erro no upload
if ($arquivo['error'] !== UPLOAD_ERR_OK) {
    redirecionarComMensagem('Erro no upload. Código: ' . $arquivo['error']);
}

// Valida tamanho (exemplo: máximo 2MB)
$tamanhoMaximo = 2 * 1024 * 1024; // 2MB em bytes

if ($arquivo['size'] > $tamanhoMaximo) {
    redirecionarComMensagem('Arquivo muito grande. Tamanho máximo: 2MB.');
}

// Valida extensão / tipo
$nomeOriginal = $arquivo['name'];

// Pega a extensão verdadeira do arquivo
$extensao = strtolower(pathinfo($nomeOriginal, PATHINFO_EXTENSION));

// Lista de extensões permitidas
$extensoesPermitidas = ['jpg', 'jpeg', 'png'];

if (!in_array($extensao, $extensoesPermitidas, true)) {
    redirecionarComMensagem('Tipo de arquivo não permitido. Envie apenas JPG ou PNG.');
}

// (Opcional) Verificar MIME type de forma extra
$tipoMimePermitidos = ['image/jpeg', 'image/png'];

$finfo = new finfo(FILEINFO_MIME_TYPE);
$tipoMimeReal = $finfo->file($arquivo['tmp_name']);

if (!in_array($tipoMimeReal, $tipoMimePermitidos, true)) {
    redirecionarComMensagem('O arquivo enviado não parece ser uma imagem válida.');
}

// Gera um novo nome único pro arquivo
$nomeSeguro = gerarNomeSeguro($extensao);

// Caminho final
$caminhoDestino = $diretorioUploads . DIRECTORY_SEPARATOR . $nomeSeguro;

// Move o arquivo temporário para o destino final
if (!move_uploaded_file($arquivo['tmp_name'], $caminhoDestino)) {
    redirecionarComMensagem('Falha ao salvar o arquivo no servidor.');
}

// Sucesso
redirecionarComMensagem('Upload realizado com sucesso!');

/**
 * Gera um nome único e seguro para o arquivo.
 */
function gerarNomeSeguro(string $extensao): string
{
    // uniqid + random_bytes deixam o nome pouco previsível
    $parteAleatoria = bin2hex(random_bytes(16));
    return $parteAleatoria . '.' . $extensao;
}

/**
 * Redireciona de volta para o index com mensagem.
 */
function redirecionarComMensagem(string $mensagem): void
{
    $mensagem = urlencode($mensagem);
    header("Location: index.php?msg={$mensagem}");
    exit;
}

O que estamos fazendo de certo aqui?

  1. Checando erro do upload ($arquivo['error'])
  2. Definindo um tamanho máximo (2MB) e validando em bytes
  3. Bloqueando extensões indesejadas (por exemplo .php, .exe etc.)
  4. Confirmando o MIME type real do arquivo com finfo
  5. Gerando um novo nome aleatório (nada de confiar no nome original do usuário)
  6. Usando move_uploaded_file() com caminho calculado de forma segura

Isso já está muito melhor do que:

move_uploaded_file($_FILES['arquivo']['tmp_name'], 'uploads/' . $_FILES['arquivo']['name']);

que é o que a maioria faz e abre várias portas pra problema.


4. Melhorando ainda mais: proteção de acesso à pasta de upload

Idealmente, arquivos de upload não deveriam ficar dentro de public/ (raiz web), para evitar que alguém acesse diretamente um .php malicioso que foi enviado por engano.

Você já colocou a pasta uploads/ fora de public:

meu_upload/
  public/
    (arquivos acessados pela web)
  uploads/
    (arquivos fora do alcance direto da URL)

Se por algum motivo você precisar que uploads/ fique dentro de public/, é interessante:

  • bloquear execução de .php nessa pasta
  • ou configurar servidor (Apache/nginx) pra tratar ali só como arquivos estáticos.

Mas, pra um cenário simples, só de colocar fora de public/ já é um ótimo avanço.


5. Como exibir as imagens enviadas

Se você salvou o nome do arquivo no banco, ou quer listar os arquivos de uploads/, pode criar algo tipo:

<?php
$diretorioUploads = __DIR__ . '/../uploads';

$arquivos = array_diff(scandir($diretorioUploads), ['.', '..']);
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <title>Imagens enviadas</title>
</head>
<body>
    <h1>Imagens enviadas</h1>

    <?php foreach ($arquivos as $arquivo): ?>
        <div style="margin-bottom: 10px;">
            <img src="../uploads/<?= htmlspecialchars($arquivo, ENT_QUOTES, 'UTF-8'); ?>"
                 alt="" style="max-width: 200px;">
        </div>
    <?php endforeach; ?>
</body>
</html>

Isso funciona se a pasta uploads/ estiver acessível pela URL.
Se ela estiver fora da pasta pública, você precisa criar um script que lê o arquivo e envia o conteúdo (tipo um “proxy de imagens”).


6. Resumindo: o fluxo de um upload moderno e seguro em PHP

  1. Criar um formulário com enctype="multipart/form-data".
  2. Conferir se $_FILES['arquivo'] existe e se não teve erro (UPLOAD_ERR_OK).
  3. Validar:
    • tamanho
    • extensão
    • tipo MIME real
  4. Gerar um nome de arquivo seguro e único (nada de usar o nome original cru).
  5. Salvar em uma pasta específica (uploads/), de preferência fora da raiz pública.
  6. Usar htmlspecialchars() ao exibir os nomes em HTML.

Com esse padrão, você já foge de:

  • upload de script malicioso sendo executado
  • arquivos gigantes travando o servidor
  • extensões estranhas
  • bagunça de arquivos com o mesmo nome se sobrescrevendo

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Rolar para cima