Em resumo: na segurança de uploads em PHP não decide uma verificação isolada, mas a combinação: verificar o tipo de ficheiro no servidor, renomear os ficheiros, guardá-los fora da raiz web e não permitir a execução de scripts na pasta de uploads. Um formulário de upload sem verificação é um dos caminhos clássicos pelos quais os atacantes levam uma backdoor para um servidor web.
Porque é que os formulários de upload são uma porta de entrada preferida
Qualquer formulário que aceite ficheiros escreve dados alheios no seu servidor. Incluem-se formulários de candidatura, portais de clientes, galerias de imagens, áreas de suporte e muitas extensões de CMS e lojas. Se um atacante conseguir colocar ali um ficheiro que o servidor web depois execute como PHP, tem uma backdoor persistente.
Para a gestão, isto significa: um upload de aparência inofensiva para currículos pode, no pior dos casos, levar à tomada de todo o servidor web. Além disso, nas pastas de uploads há muitas vezes dados pessoais que, com uma configuração errada, ficam acessíveis ao público.
Para administradores e programadores significa: o código da aplicação e a configuração do servidor têm de condizer. Uma fragilidade num nível é muitas vezes compensada pelo outro, se ambos estiverem bem implementados. Como encontrar ficheiros já introduzidos, descreve-o o guia Detetar e remover webshells.
As regras básicas da segurança de uploads em PHP
As regras seguintes correspondem às recomendações das OWASP Cheat Sheets e têm-se revelado eficazes em operação:
- Lista de permissão em vez de lista de bloqueio: aceitar apenas tipos de ficheiro expressamente permitidos, por exemplo PDF, JPG e PNG. As listas de bloqueio de extensões isoladas podem ser contornadas com variantes e extensões duplas.
- Verificar o conteúdo, não apenas a extensão: determinar o tipo real no servidor com
finfo. O tipo MIME em$_FILESé uma indicação do remetente e não é fiável. - Novo nome de ficheiro: nunca usar o nome original como nome de ficheiro. Gerar um nome aleatório e acrescentar a extensão da lista de permissão.
- Armazenamento fora da raiz web: guardar os ficheiros de modo que não sejam acessíveis diretamente por URL. A entrega faz-se através de um script com verificação de permissões.
- Limitar o tamanho: na aplicação, no PHP e no servidor web.
- Sem execução: se os uploads tiverem mesmo de ficar na raiz web, desativar a execução de scripts nesse diretório.
- Verificação apenas no servidor: as verificações em JavaScript no navegador melhoram a utilização, mas não protegem.
Um exemplo seguro com finfo e nome aleatório
O exemplo seguinte mostra o núcleo de um processamento seguro. É deliberadamente conciso e tem de ser integrado na sua aplicação, ou seja, com autenticação, verificação de permissões, proteção CSRF e registo.
<?php
declare(strict_types=1);
const MAX_BYTES = 5 * 1024 * 1024; // 5 MB
const ERLAUBT = [
'application/pdf' => 'pdf',
'image/jpeg' => 'jpg',
'image/png' => 'png',
];
const ZIELORDNER = '/var/www/daten/uploads'; // fora da raiz web
$datei = $_FILES['dokument'] ?? null;
if (!is_array($datei) || $datei['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
exit('Upload fehlgeschlagen.');
}
if ($datei['size'] > MAX_BYTES || !is_uploaded_file($datei['tmp_name'])) {
http_response_code(400);
exit('Datei ungültig.');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$typ = $finfo->file($datei['tmp_name']);
if (!isset(ERLAUBT[$typ])) {
http_response_code(415);
exit('Dateityp nicht erlaubt.');
}
$name = bin2hex(random_bytes(16)) . '.' . ERLAUBT[$typ];
$ziel = ZIELORDNER . '/' . $name;
if (!move_uploaded_file($datei['tmp_name'], $ziel)) {
http_response_code(500);
exit('Speichern fehlgeschlagen.');
}
chmod($ziel, 0640);
// Guardar o nome original apenas como texto na base de dados
// e escapá-lo com htmlspecialchars() em cada saída.
Para imagens vale a pena um passo adicional: voltar a codificar a imagem com GD ou Imagick, descartando os metadados. Isso não substitui as outras verificações, mas reduz o risco residual de dados alheios incorporados.
Para documentos de fontes desconhecidas, como candidaturas ou fotos de reclamações, é útil, além disso, uma verificação antivírus antes de os reencaminhar. Protege menos o servidor web do que os colaboradores que mais tarde abrem os ficheiros nos seus computadores de trabalho. Registe ainda cada upload com hora, conta, tamanho do ficheiro e tipo detetado, para que padrões invulgares possam ser reconstituídos mais tarde.
Nos CMS e lojas, as bibliotecas de multimédia integradas trazem em geral verificações próprias. Mais arriscadas são as extensões de formulários, as funções de importação e as páginas adicionais programadas à medida, que guardam ficheiros contornando a biblioteca. São precisamente estes pontos que deve examinar primeiro numa verificação.
Entregue os ficheiros da pasta protegida com Content-Disposition: attachment e X-Content-Type-Options: nosniff. Assim, o navegador não mostra diretamente conteúdos problemáticos, mas oferece-os como download.
Desativar a execução de PHP na pasta de uploads
Mesmo que um ficheiro com conteúdo falso passe, o servidor web não o pode executar como script. Esta segunda camada de proteção pertence a todos os servidores que guardam uploads na raiz web.
Para Nginx com PHP-FPM:
location ^~ /uploads/ {
location ~* \.(php|phtml|phar)$ {
deny all;
}
}
Para Apache, de preferência diretamente no VirtualHost e não num .htaccess, para que a regra não possa ser substituída pela aplicação:
<Directory "/var/www/html/uploads">
AllowOverride None
<FilesMatch "\.(php|phtml|phar)$">
Require all denied
</FilesMatch>
</Directory>
Se o PHP correr como módulo do Apache (mod_php), pode definir adicionalmente php_admin_flag engine off no mesmo bloco. Depois da alteração, verifique com um ficheiro de teste inofensivo, que apenas devolve um texto, que o servidor responde a um pedido sobre ele com 403.
Limites de tamanho e a relação com o PHP2Shell
Os uploads estão estreitamente ligados às definições PHP post_max_size e upload_max_filesize. No contexto do PHP2Shell, o php.net recomenda como medida transitória definir post_max_size para 8M quando não são necessários uploads, ou para 32M com uploads, e manter o upload_max_filesize abaixo disso. Não é uma proteção completa e não substitui a atualização para uma versão corrigida do PHP.
; php.ini ou pool FPM (php_admin_value), depois reiniciar todos os workers
file_uploads = On
post_max_size = 32M
upload_max_filesize = 20M
max_file_uploads = 5
Se um site não precisar de uploads, o file_uploads = Off é a solução mais limpa. Que definições fazem sentido até ao patch, está em Medidas imediatas sem patch. Permissões adequadas para diretórios de uploads são descritas em Permissões de ficheiros no servidor web.
Erros típicos nas funções de upload
Ao rever aplicações existentes, surgem repetidamente os mesmos padrões. Verifique os seus formulários de forma orientada quanto a estes pontos:
- A extensão só é verificada parcialmente, de modo que passam nomes de ficheiro com várias extensões.
- O nome original do formulário acaba no caminho de armazenamento. Isso abre caminhos para a manipulação de caminhos e para a substituição de ficheiros existentes.
- Os uploads estão públicos com nomes previsíveis. Em candidaturas ou contratos, isso é também um problema de proteção de dados.
- A pasta de uploads é gravável pelo servidor web e, ao mesmo tempo, executável.
- Extensões de upload antigas ou plugins de formulários não são atualizados, embora o fabricante tenha publicado atualizações de segurança.
- Os erros não são registados. Os uploads rejeitados são, porém, um sinal precoce de tentativas dirigidas.
Como ajuda concretamente a PHP SOS?
- Verificamos as suas funções de upload no código e na configuração do servidor e indicamos fragilidades concretas, com prioridade.
- Aplicamos o bloqueio de execução na pasta de uploads, permissões adequadas e limites de tamanho no âmbito do reforço de segurança do servidor.
- Em caso de suspeita, procuramos nos diretórios de uploads scripts introduzidos e limpamo-los através da nossa remoção de malware.
- Se desejar, mandamos verificar a implementação, de forma independente, através de um teste de intrusão.
Perguntas frequentes
Basta verificar a extensão do ficheiro no upload?
Não. A extensão e o tipo MIME comunicado pelo navegador vêm do remetente e podem ser escolhidos livremente. Verifique o conteúdo no servidor, atribua um novo nome de ficheiro e bloqueie a execução de scripts no diretório de uploads.
Onde devem ser guardados os ficheiros carregados?
O melhor é fora da pasta web pública. A entrega fica então a cargo de um script que verifica antes se o utilizador pode ver o ficheiro. Assim, os ficheiros não são nem diretamente acessíveis nem executáveis.
Os ficheiros SVG como imagens são inofensivos?
Não automaticamente. O SVG é XML e pode conter conteúdo ativo. Só permita SVG se limpar os ficheiros ou se os entregar exclusivamente como download, com o cabeçalho Content-Disposition adequado.
O tamanho do upload tem alguma relação com o PHP2Shell?
Sim, indiretamente. O php.net recomenda, como medida transitória, valores mais baixos para post_max_size e upload_max_filesize. Isso reduz o risco, mas não substitui a atualização para uma versão corrigida do PHP e o reinício de todos os workers.