Upload de arquivos
Esta aula ensina como realizar upload de arquivos em PHP, abordando o array superglobal $_FILES, a função move_uploaded_file, validação de tipo e tamanho, e os principais riscos de segurança. Inclui exemplos práticos e exercícios com respostas.
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çãomime_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. Usebasename()para limpar o nome e gere nomes únicos comuniqid()oumd5(). - 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
finfooumime_content_type(). - Verifique se o arquivo é uma imagem válida com
getimagesize().
Referências
- PHP: $_FILES - Manual
- PHP: move_uploaded_file - Manual
- PHP: Upload de arquivos - Manual
- OWASP: Unrestricted File Upload
- PHP: mime_content_type - Manual
- PHP: finfo_file - Manual
Exercícios
- 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.
- Explique por que é perigoso usar o nome original do arquivo (ex:
$_FILES['arquivo']['name']) diretamente como destino emmove_uploaded_file(). Dê um exemplo de ataque. - 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. - Qual é a diferença entre
$_FILES['arquivo']['type']e o tipo MIME obtido porfinfo_file()? Por que o segundo é mais seguro? - Implemente um sistema de upload que rejeite arquivos com extensão
.php,.phtml,.php5e que também verifique o tipo MIME real para garantir que não sejam scripts disfarçados. Usepathinfo()efinfo.
<!-- 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.';
}
?>
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.
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);
}
$_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().
$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...