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
includemal 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
- Defina a lista do formulário, menor que a lista geral do site. Currículo não precisa de vídeo nem de planilha.
- Confira o nonce e o tamanho antes de tocar no conteúdo.
- Peça ao WordPress para comparar nome e conteúdo com
wp_check_filetype_and_ext(). - Confirme o tipo real com
finfo, que lê os primeiros bytes do arquivo. - 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. - 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 mandaimage/jpegpara tudo. - Checar extensão com
strpos: aceitafoto.jpg.phpe 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_UPLOADSpara "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
finfoou 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.phpna pasta de uploads devolve 403, não executa. ALLOW_UNFILTERED_UPLOADSnão está definido nowp-config.phpde 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?
Referência: https://codex.wordpress.org/Function_Reference/get_allowed_mime_types