Validação defensiva · 25 de outubro de 2026

ServiStar Members Site

Análise do código custom de members.servistarconsulting.com: os 11 achados P0 do roadmap, o que eu confirmei linha por linha, e o que precisa ser corrigido antes de gravar a demonstração para o cliente.

01

Resumo

O material entregue é tecnicamente sólido, mas não roda como está: a suíte morre num erro fatal e o patch principal corrompe uma linha de JavaScript.

11achados P0 no roadmap — 4 críticos
21handlers nopriv só no child theme
9 → 11testes vermelhos → verdes com os patches
2bugs que travam a demonstração

Confirmei no código os 11 itens da lista P0. Os quatro que já têm prova automatizada (P0-1, P0-5, P0-6, P0-10) se comportam exatamente como o relatório descreve. Os outros seis também estão lá, com as linhas citadas.

A parte boa: depois de corrigir dois defeitos no próprio material, a suíte fecha 11 de 11 verdes com os quatro patches aplicados. O ciclo vermelho → verde funciona de verdade — não é só documentação.

O ponto central para a gravação: a suíte não faz nenhuma requisição HTTP. Ela extrai a função real do arquivo com o tokenizer do PHP e roda contra stubs do WordPress. O Cloudflare nunca vê nada, então o bloqueio de WAF deixa de ser um problema para o vídeo.

02

Os dois áudios

Transcritos do WhatsApp. Contexto do pedido: validar os problemas críticos e achar uma forma de reproduzir ao vivo, já que o Cloudflare barra os testes contra o site.

Áudio 1

41 s

“Queria te pedir um favor, se pudesse me ajudar, tu que já teve uma experiência com PHP. O projeto que eu peguei lá do ServiStar tá acusando vários erros de vários problemas críticos de segurança, tá ligado? Só que eu não consigo validar com Cloud porque ele identifica como eu querendo atacar o site. Eu queria explorar isso para mostrar para o cliente ao vivo como é que é. Eu pensei que talvez alguma das IAs japonesas, japonesas, não, chinesas, pudesse ajudar, talvez o Kim, alguma coisa. Se importar, não se importava, se importa de eu te mandar o repo do projeto e tu analisar, ver se encontra os mesmos ataques críticos ali, problemas críticos, e alguma forma de replicar isso num vídeo.”

Áudio 2

4 s

“Ajudar teu amigo aí a ganhar o pão de cada dia.”

03

O que é esse projeto

Snapshot do código custom de um site WordPress B2B que vende assentos para organizações — cada organização compra um lote e o admin dela cadastra o próprio time.

Stack

WordPress sobre Cloudways Autonomous. BuddyBoss child theme, LearnDash (cursos), WooCommerce + Subscriptions (assentos), Groundhogg (CRM e árvore de organizações), WP Fusion, JetEngine e Elementor.

Quem fez

Child theme e plugins custom são da agência Tribe Pub. O plugin custom-learndash é terceiro — cabeçalho “Larry Jacob LearnDash Toolkit”. O gamplify-rpm é comercial da Tribe Pub, editado direto no lugar.

Estado do repo

Fonte de verdade desde 21/10/2026; antes o código só existia no servidor. O zip veio do branch test/security-p0-validation, com testes e patches ainda não commitados.

Parte do comportamento do site vive no banco — CPTs do JetEngine, funis do Groundhogg, WP Fusion e Elementor. O que depende disso está marcado como não provado pelo código nos documentos originais.

04

Os 11 achados P0

Todos conferidos no código desta cópia. A coluna “onde” aponta arquivo e linha para o dev da Tribe Pub conseguir abrir direto.

Achado Severidade Onde O que acontece
P0-1 Crítico member-management.php:227, :787, :1089, :1122 Quatro ações registradas em wp_ajax_nopriv_* sem nonce, capability ou checagem de árvore. Apaga qualquer usuário, promove a sub-admin, cria contas em massa e “reivindica” usuários.
P0-2 Crítico servievent-functionality.php:6, :1143 · servicourse:660, :1191 Handlers legados de assento também em nopriv; dois deles despejam SELECT * FROM gh_contacts.
P0-3 Alto servistar-manage-course-order-list.php:37-61 · child-user-list.php:11 Interruptores de debug na URL: ?order_id imprime dados, ?clean=1 apaga as matrículas do pedido, owner_id_dev lista os membros de outro dono.
P0-4 Crítico class-gamplify-rpm.php:163-228 · handlers em public:262, :5697, :5993, :6265 Quatro endpoints de relatório em nopriv montam SQL com $_POST cru e devolvem e-mails de contatos. O registro fica no arquivo includes, não no public.
P0-5 Crítico group_course.php:17, :34, :45 · group_creation.php:15, :19 Templates de página concatenam $_REQUEST em SQL sem login. Também dão group_leader a qualquer e-mail e publicam cursos com conteúdo arbitrário.
P0-6 Alto account_creation.php:329-335 · free_rsvp…:487-491 Segredo do reCAPTCHA literal no código e troca de ambiente por comentário de linha. No repo os valores são REDACTED, mas continuam nos servidores.
P0-7 Alto free_rsvp_event_registerations.php:15, :20 (render) · handler sem checagem paid_free só é validado ao renderizar o formulário. POST direto no AJAX inscreve num evento pago e entrega a tag de acesso. wp_create_user sem tratar WP_Error pode sobrescrever o usuário 1.
P0-8 Médio servistar-custom-functions.php:2390-2425 Logs com e-mails gravados dentro do tema (.log e .log.old), provavelmente alcançáveis por HTTP.
P0-9 Alto functions.php:438-507 Dono da organização inferido pelo domínio do e-mail, sem excluir domínios públicos. Em produção: 13 usuários sales_rep com domínio público administrando contatos.
P0-10 Baixo functions.php:2118-2137 Gate do REST procura no REQUEST_URI inteiro, então a query string satisfaz o whitelist: ?x=/wp-json/wc/ passa. Só restaura o padrão do WordPress.
P0-11 Alto trusted-member-function.php:1024-1050 create_new_user() re-parenteia usuário que já existe: grava new_owner e troca o owner_id no Groundhogg. Qualquer logado dispara isso pelo RSVP “outra pessoa”.
P0-1 — a manchete Crítico

Apagar usuário sem estar logado

O handler lê $_POST['userwpdelete'], converte para inteiro e chama wp_delete_user() direto. Não há check_ajax_referer, current_user_can nem verificação de árvore em nenhum dos quatro handlers.

Como o admin-ajax.php carrega wp-admin/includes, a função existe no contexto do AJAX. Um POST anônimo apaga administrador.

P0-5 — SQLi clássico Crítico

Concatenação direta em SQL

post_title like '".$coursename."' — sem prepare, sem escape, sem login. O template renderiza para qualquer visitante que abrir a página.

Vale checar antes se as páginas existem: o roadmap marca isso como decisão (query Q1). Se ninguém usa o toolkit, desativar o plugin é a saída mais barata.

P0-6 — higiene Alto

Segredo no código

O valor está literal e a escolha entre produção e staging é feita comentando linhas. No repositório tudo virou REDACTED e o histórico foi limpo com filter-repo.

O risco é operacional: as chaves reais seguem nos servidores e nas cópias do dev anterior. Rotacionar no console do Google é obrigatório, não opcional.

P0-10 — o mais fácil de filmar Baixo

Bypass do gate REST

strpos() no URI completo faz a query string valer como rota. É o único que roda com PHP puro, sem includes nem template — por isso o roadmap recomenda gravar esse primeiro, para estabelecer o formato.

05

A suíte de testes

Executei com PHP 8.2.27 e PHPUnit 9.6.38 locais. O README avisa que nunca havia sido rodada — e isso apareceu na primeira tentativa.

Defeito 1 — a suíte não carrega

tests/security/bootstrap.php:328 declara namespace Groundhogg; no meio do arquivo, depois de código global. Em PHP isso é erro fatal e a execução morre antes do primeiro teste.

# PHP Fatal error
Namespace declaration statement has to be the very first statement
or after any declare call in the script in tests/security/bootstrap.php:328

Correção: mover o bloco Groundhogg (linha 327 até o fim) para tests/security/groundhogg-stubs.php começando com <?php + namespace Groundhogg;, e no bootstrap trocar por:

+ require_once __DIR__ . '/groundhogg-stubs.php';

Defeito 2 — o patch principal está corrompido

patches/P0-01-member-management-auth.patch:22 traz um dump hexadecimal colado no meio da linha de JavaScript do upload de CSV. O patch aplica limpo e os testes passam, porque nenhum teste executa JS — mas o admin quebra.

- formData.append('csv_file', 20 20 12 61 79 80 81 33 98 100 204 250 395 398 399 400'#csv_file')[0].files[0]);
+ formData.append('csv_file', $('#csv_file')[0].files[0]);

Resultado medido

Teste Código atual Com o patch
P0_01 · anônimo não apaga usuário Falha Passa
P0_01 · anônimo não vira sub-admin Falha Passa
P0_05 · input não chega cru no SQL Falha Passa
P0_05 · anônimo não toca no banco Falha Passa
P0_06 · segredo não é literal (2 arquivos) Falha Passa
P0_06 · segredo vem de configuração (2 arquivos) Falha Passa
P0_10 · rota bloqueada não vaza pela query Falha Passa
P0_10 · rota realmente bloqueada continua bloqueada Passa Passa
P0_10 · rota WooCommerce legítima continua liberada Passa Passa

Números: linha de base com 11 testes e 9 falhas (16 asserções). Com os quatro patches aplicados: OK — 11 testes, 18 asserções. Os quatro patches aplicam limpo com git apply --check.

O README fala em “4 testes falhando, um por achado”. Na prática são 9 métodos, porque cada achado tem mais de uma asserção. Vale ajustar o texto para não gerar expectativa errada no vídeo.

06

O que os patches ainda deixam em aberto

Nada disso invalida a demonstração, mas o dev da Tribe Pub vai levantar. Melhor chegar antes.

  • Reivindicar usuário sem filtro de domínio. O patch põe nonce e CheckMultipleGroupTags() em assing_existing_user_to_owner, mas não reaplica o filtro de mesmo domínio e não-possuído que a própria tela usa em member-management.php:345-379. O roadmap pede isso explicitamente.
  • Delete só checa o usuário do WordPress. A verificação de árvore cobre userwpdelete, mas não o contato do Groundhogg em userghdelete. Ainda é possível apagar contato alheio por ID.
  • P0-05 exige manage_options e faz return. Se essas páginas fazem parte de um fluxo público de parceiro, o patch quebra a funcionalidade. O roadmap já marca como decisão, dependente da query Q1.
  • Revogação de sub-admin. O patch remove o group_leader no caminho de revogar — bom — mas vale revisar se sobra new_owner = 0 em algum fluxo antigo.
  • P0-2, P0-3, P0-4, P0-7, P0-8, P0-9 e P0-11 estão sem teste e sem patch. São 7 dos 11 achados ainda sem prova automatizada.
07

Plano de gravação

O ciclo vermelho → verde acontece inteiro na máquina local. Para o que precisar de site real, staging com Cloudflare em modo de desenvolvimento.

  1. Corrigir o bootstrap e o patch P0-01Antes de qualquer gravação. Sem isso a suíte não abre e o admin quebra ao demonstrar o upload de CSV.
  2. Gravar P0-10 primeiroÉ o único que roda com PHP puro: sem includes, sem template. O vermelho → diff → verde é imediato e estabelece o formato. Baixo risco se precisar repetir a tomada.
  3. Gravar P0-1 em seguidaÉ a manchete. A saída vermelha diz literalmente que uma requisição anônima chegou em wp_delete_user(). É o momento mais visceral para o COO.
  4. Depois P0-5SQLi clássico, o que os devs da Tribe Pub reconhecem na hora. O payload zzz' OR '1'='1 lê como livro-texto no diff.
  5. P0-6 por últimoÉ contrato estático, não exploit ao vivo. Narrar como higiene e rotação de chave, não como ataque — evita over-claim na câmera.
  6. Se precisar do site realStaging + Development Mode no Cloudflare (ou hostname não-proxied) durante a janela de gravação, com autorização escrita do cliente. Nunca o payload destrutivo em produção — usar usuário descartável.

Por que o Cloudflare deixa de ser problema: os testes leem a função direto do arquivo fonte e rodam contra stubs de wp_delete_user, $wpdb, wp_fusion() e afins. Não existe requisição, não existe IP, não existe WAF. Um php phpunit.phar -c tests/security/phpunit.xml.dist basta.

08

Higiene do pacote

O zip foi gerado da árvore de trabalho e levou junto arquivos que o .gitignore exclui.

  • scripts/.sync.env — contém o host SSH e o caminho absoluto da raiz do WordPress em produção. Não são credenciais, mas são detalhes de infraestrutura. Está no .gitignore e não está versionado.
  • .claude/settings.local.json — configuração local, também ignorada pelo git, mas presente no zip.
  • .DS_Store e histórico do filter-repo — ruído, sem impacto.

Varredura de segredos

Procurei padrões de chave do Google (AIza…) e literais longos de secret, api_key e password no child theme e nos plugins. Nenhuma ocorrência na árvore de trabalho — os valores do reCAPTCHA estão como REDACTED, coerente com o que o relatório afirma.

09

Próximo passo

1. Destravar a suíte

Mover o bloco Groundhogg para um arquivo próprio e consertar a linha do formData. Duas edições pequenas, ambas em arquivos de teste — nada toca no código de produção.

2. Fechar as lacunas do P0-1

Reaplicar o filtro de domínio no claim e checar o contato do Groundhogg no delete. São os dois buracos que sobrevivem ao patch atual.

3. Confirmar a Q1

Saber se o custom-learndash está ativo e se as páginas existem decide entre desativar o plugin ou aplicar o gate de administrador.

Escopo: tudo aqui foi verificado em cópia local. Nenhum comando foi executado contra produção, e os patches seguem como proposta para revisão — nada foi aplicado no repositório do Henrique.