Programação Por Equipe DevBras

Capa do artigo: Upload no WordPress: a lista de tipos permitidos não confere o que vem dentro

Upload no WordPress: a lista de tipos permitidos não confere o que vem dentro

Como usar a lista de MIME do WordPress como começo da regra: conferir o conteúdo, limitar o tamanho, barrar SVG sem saneamento e impedir execução na pasta de uploads.

O site da escola recebe currículos por um formulário feito num plugin próprio. A regra era simples: só PDF, JPG e PNG. O código conferia se o nome terminava em uma dessas extensões e gravava em wp-content/uploads. Um dia apareceu lá um curriculo.pdf que, aberto no editor, era uma página HTML com script. Ele passou porque a pergunta feita foi "como o arquivo se chama?", e não "o que tem dentro?".

A resposta curta: a lista de tipos permitidos é o começo da política, não a política inteira. No WordPress, passe o upload por wp_check_filetype_and_ext() e wp_handle_upload() com uma lista restrita, confira o conteúdo com finfo, limite o tamanho antes de processar, não aceite SVG que você não saneia e garanta que nada na pasta de uploads seja executado pelo servidor.

Por que a extensão engana

Extensão é parte do nome, e o nome quem escolhe é a pessoa que envia. Renomear script.html para script.pdf leva um segundo. O campo type de $_FILES também vem do navegador, então qualquer cliente pode mandar image/jpeg junto com um arquivo que não é imagem.

O WordPress tem uma lista central de extensões e tipos aceitos, que você consulta com get_allowed_mime_types() e altera pelo filtro upload_mimes. Ela é útil: define o que a biblioteca de mídia aceita. Mas ela relaciona extensão a tipo declarado. Se o seu plugin só compara o final do nome com essa lista, você herdou a lista e ignorou a verificação.

Há ainda o erro clássico de checagem frouxa. Um strpos($nome, '.jpg') !== false aceita foto.jpg.php. Se esse arquivo for parar numa pasta onde o servidor executa PHP, quem enviou ganhou um terminal no seu site. O post sobre MIME type e extensão explica o conceito em geral. Aqui o foco é o que fazer dentro do WordPress.

O que pode dar errado com cada tipo

  • PHP disfarçado: só vira execução se o servidor rodar aquele arquivo, por extensão dupla, configuração frouxa ou um include mal feito. Por isso a pasta de upload não pode executar nada.
  • HTML e SVG: não precisam de servidor para causar dano. Servidos do seu domínio, rodam script no navegador de quem abrir, com acesso aos cookies e à sessão daquele domínio.
  • Arquivo gigante: uma imagem de 15 mil por 15 mil pixels pode derrubar a geração de miniaturas por memória, mesmo sendo uma imagem legítima.

Como receber o arquivo com a regra inteira

  1. Defina a lista do formulário, menor que a lista geral do site. Currículo não precisa de vídeo nem de planilha.
  2. Confira o nonce e o tamanho antes de tocar no conteúdo.
  3. Peça ao WordPress para comparar nome e conteúdo com wp_check_filetype_and_ext().
  4. Confirme o tipo real com finfo, que lê os primeiros bytes do arquivo.
  5. Grave com wp_handle_upload(), passando a mesma lista, para o nome final ser saneado e a gravação seguir o fluxo do núcleo.
  6. Bloqueie a execução de scripts na pasta de uploads no servidor web.

No plugin da escola, o tratamento ficou assim:

require_once ABSPATH . 'wp-admin/includes/file.php';

$permitidos = array(
    'pdf'          => 'application/pdf',
    'jpg|jpeg|jpe' => 'image/jpeg',
    'png'          => 'image/png',
);

if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'enviar_curriculo' ) ) {
    wp_die( 'Sessão expirada. Recarregue a página.' );
}

$arquivo = $_FILES['curriculo'];

if ( $arquivo['error'] !== UPLOAD_ERR_OK || $arquivo['size'] > 5 * 1024 * 1024 ) {
    wp_die( 'Envie um arquivo de até 5 MB.' );
}

$checagem = wp_check_filetype_and_ext( $arquivo['tmp_name'], $arquivo['name'], $permitidos );
if ( ! $checagem['ext'] || ! $checagem['type'] ) {
    wp_die( 'Tipo de arquivo não permitido.' );
}

$finfo = new finfo( FILEINFO_MIME_TYPE );
$real  = $finfo->file( $arquivo['tmp_name'] );
if ( ! in_array( $real, array_values( $permitidos ), true ) ) {
    wp_die( 'O conteúdo não corresponde a PDF, JPG ou PNG.' );
}

$resultado = wp_handle_upload( $arquivo, array(
    'test_form' => false,
    'mimes'     => $permitidos,
) );

if ( isset( $resultado['error'] ) ) {
    wp_die( esc_html( $resultado['error'] ) );
}

Com isso, o curriculo.pdf que era HTML é recusado: o finfo responde text/html, que não está na lista. O foto.jpg.php nem chega a ser considerado, porque a extensão final é php.

O wp_check_filetype_and_ext() lê o conteúdo de imagens para descobrir o tipo real, e versões atuais também consultam o fileinfo do PHP para outros tipos quando a extensão está disponível. A checagem explícita com finfo no seu código deixa a regra visível e não depende de detalhe de versão.

Ajustando a lista geral do site

Se o problema é a biblioteca de mídia aceitar demais, use o filtro upload_mimes para tirar o que ninguém do site precisa enviar:

add_filter( 'upload_mimes', function ( $mimes ) {
    unset( $mimes['exe'], $mimes['swf'] );
    return $mimes;
} );

Remover é seguro. Adicionar exige o mesmo cuidado de antes: cada tipo novo é uma pergunta sobre como ele será servido e aberto.

SVG: o tipo que parece imagem e é documento

O WordPress não aceita SVG por padrão, e não é descuido. SVG é XML e pode carregar script, manipuladores de evento e links externos. Plugins que "liberam SVG" com uma linha no upload_mimes abrem exatamente essa porta.

Se o site precisa de SVG, restrinja o envio a quem administra, passe cada arquivo por um saneador mantido e ativo e prefira entregá-lo como imagem (img) em vez de incorporá-lo inline. Para envio público, como o formulário de currículo, simplesmente não aceite.

Pasta de uploads que não executa nada

Mesmo com a validação certa, trate a pasta de uploads como hostil. No Apache, um .htaccess dentro de wp-content/uploads recusa pedidos a arquivos de script:

<FilesMatch "[.](php|phtml|phar)$">
    Require all denied
</FilesMatch>

No Nginx, o bloqueio entra no bloco do site, antes do location que repassa PHP para o PHP-FPM, porque a primeira expressão regular que casar vence:

location ~* ^/wp-content/uploads/.*[.]php$ {
    deny all;
}

Teste depois: crie um teste.php com <?php echo 1; na pasta e abra no navegador. A resposta certa é 403. Se aparecer o número 1, a pasta executa código. Depois, apague o arquivo de teste.

Erros comuns

  • Confiar em $_FILES['type']: o valor vem do cliente. Qualquer script de ataque manda image/jpeg para tudo.
  • Checar extensão com strpos: aceita foto.jpg.php e similares. Se o servidor executar, o site é de outra pessoa.
  • Reaproveitar a lista geral num formulário público: o visitante anônimo passa a enviar tudo o que um editor envia.
  • Liberar SVG por filtro sem saneamento: qualquer SVG enviado vira script rodando no seu domínio.
  • Definir ALLOW_UNFILTERED_UPLOADS para "resolver" um tipo recusado: desliga a filtragem para quem tem a permissão correspondente e esconde o problema real.
  • Processar antes de limitar tamanho: a geração de miniaturas estoura a memória e derruba o PHP no meio de uma requisição comum.

Checklist

  • O formulário tem uma lista própria de tipos, menor que a do site.
  • Nonce e tamanho são conferidos antes de qualquer leitura do arquivo.
  • O tipo real é confirmado pelo conteúdo, com finfo ou equivalente.
  • A gravação passa por wp_handle_upload() com a mesma lista.
  • SVG é recusado no envio público ou passa por saneador mantido.
  • Um teste.php na pasta de uploads devolve 403, não executa.
  • ALLOW_UNFILTERED_UPLOADS não está definido no wp-config.php de produção.

Para a parte genérica de PHP, fora do WordPress, veja upload de arquivo em PHP, e para imagens vale também regravar a imagem em vez de guardar os bytes originais.

Pegue um arquivo de texto qualquer, renomeie para .jpg e envie pelo formulário do seu site agora: ele é recusado ou aparece na biblioteca de mídia?

Avalie este artigo

Referência: https://codex.wordpress.org/Function_Reference/get_allowed_mime_types