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.
nopriv só no child themeConfirmei 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.
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.”
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.
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”. |
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.
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.
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.
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.
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.
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()emassing_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 emmember-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 emuserghdelete. Ainda é possível apagar contato alheio por ID. - P0-05 exige
manage_optionse fazreturn. 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_leaderno caminho de revogar — bom — mas vale revisar se sobranew_owner = 0em 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.
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.
- 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.
- 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.
- 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. - Depois P0-5SQLi clássico, o que os devs da Tribe Pub reconhecem na hora. O payload
zzz' OR '1'='1lê como livro-texto no diff. - 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.
- 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.
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_Storee histórico dofilter-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.
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.