← Relatórios

Achados da suíte de testes — Monolito (SGR)

Bugs e comportamentos anômalos descobertos pela suíte de testes automatizados do monolito EasyJur (branch tests/suite-monolito). Atualizado em 16/06/2026. Cobertura priorizada pelo tráfego real de produção (ingress-nginx): Agenda ≈ 40% do uso.

🟢→🔴 A suíte virou a lista viva de bugs ("sem verde")

Os testes que antes fotografavam o bug (assertavam o valor errado pra passar verde) agora assertam o comportamento correto/seguro e ficam VERMELHOS até o fix. São 60 testes vermelhos rastreando estes achados — cada um vira verde sozinho quando o bug for corrigido. O gate de regressão (que exclui os bugs conhecidos) segue 100% verde, então um vermelho novo = quebra real. Rode ./run-all.sh --bugs pra ver a lista viva. Nova camada de testes JS (node:test, zero deps) cobre o cálculo financeiro (arredondamento.js), formatadores de dinheiro/data e parsers — e já achou o B7.

11
🔴 Críticos (segurança)
7
🟠 Bugs de lógica
4
🟡 Robustez/contrato
4
⚪ Dívida/comportamento

⚠ Levar pro time agora

  1. S7SQL injection EXPLORÁVEL em autoCompleteCidades.php: term=zzz%' or 1=1 -- retornou 5.570 linhas (vs 20). Exfiltração confirmada, sem prepared statement.
  2. S9Gateway central de escrita mysql_manager.php (62 hits) sem guard de sessão e lendo id_empresa do POST → anônimo poderia escrever em ~26 tabelas de qualquer tenant (cross-tenant). Contido hoje só por proteção acidental.
  3. S11SQL injection SISTÊMICO na família autoComplete*: 35 dos 60 não-testados interpolam input cru; 11+ confirmados exploráveis (exfil de tabela global inteira, autoCompleteClienteJson vazou 6831 pessoas cross-tenant, escritorios/* vazam 192 emails a anônimo). Fix único: prepared statements na família toda.
  4. S6 — Vazamento cross-tenant confirmado com dado real: anônimo leu a contagem de despesas recorrentes do tenant 1 (~1894) via busca_quantidade_despesas_recorrentes.php.
  5. S1 — Mesmo padrão no plano de contas (id_empresa via POST, sem login).
  6. S4RequiredData.php grava na sessão a partir de input cru, sem auth (session poisoning).
  7. B1parse_percentual_retencao come a parte inteira (1.50 → 0,50%) — cálculo fiscal.
  8. B5 — Validação de CNPJ/CPF só checa comprimento, não o dígito verificador.
#SevResumoStatus
S1crítPlano de contas: anônimo lê de qualquer escritório (id_empresa via POST, sem login)aberto
S2crít/api/* sem Bearer → "Token de invalido" em texto + HTTP 200 (devia 401)aberto
S3crít~9 ajax_*.php/autocomplete soltos respondem sem sessãoaberto
S4crítRequiredData.php: session poisoning de input cru, zero guardaberto
S5crítapi/agenda_lista.php sem sessão vaza fragmento de SQL no corpoaberto
S6crítDespesas recorrentes: anônimo leu contagem REAL do tenant 1 (~1894) — vazamento confirmadoaberto
S7crítSQL injection explorável em autoCompleteCidades.php (term) — exfiltração confirmada (5.570 linhas)aberto
S8crítXSS refletido em getProcessoTo_CkEDITOR.php (texto_final ecoado sem escape)aberto
S9crítGateway central de escrita mysql_manager.php sem guard + id_empresa do POST → escrita CROSS-TENANT anônima (~26 tabelas); idem ajax_atualiza_prazos (prazos!), cria_chat, popup, grupos, processos_listaaberto
S10crítXSS refletido em ajax_modal_grupos.php (modulo_origem ecoado cru)aberto
S11crítSQLi sistêmico na família autoComplete*: 35/60 com input cru, 11+ exploráveis (exfil global, cross-tenant 6831 linhas, auth gap escritorios/*, echo $sql)aberto
B1altoparse_percentual_retencao: come parte inteira (fiscal)aberto
B2altoconverteCampoTexto: remove quebra de linha sem espaço, gruda palavrasaberto
B3altodata_inicio_agenda: código morto no deslocamento negativoaberto
B4altoBusca de cidade falha por mojibake em tb_cidadesaberto
B5altoValidação CNPJ/CPF só checa comprimento, não o dígito verificadoraberto
B6altoparse_format_number('1.234,56')→R$ 1,23; ajax_filter_adv chama ShowErro() inexistenteaberto
B7altoJS extrair_nome_plano_contas: split('-') trunca nome com hífen ("Receitas - Diversas"→"Receitas")aberto
R1médHTTP sempre 200, mesmo em erro (código só no corpo)aberto
R2médContratos inconsistentes: 500 vs 400, Fatal servido como HTML em 200aberto
R3médShape no vazio ([] vs null) e content-type text/html em JSONaberto
R4médUncaught TypeError escapa do catch(Exception) → stack trace + path em HTTP 200 (mysql_manager, processos_lista, cria_chat)aberto
C1dívAbrir modal "+ Novo" já grava rascunho no bancoaberto
C2dívAgenda: exclusão soft-delete + busca não filtra status (mostra apagado)aberto
C3dívFunções duplicadas entre módulos com implementações divergentesaberto
C4dívModal de publicações: aba "Principal" rouba o foco após carregaraberto

🔴 Segurança / isolamento de tenant

S1 · Vazamento cross-tenant no plano de contas

sgr/advogados/scripts/plano/api/listar_plano_contas.php · buscar_proximo_numero_plano_contas.php

id_empresa do POST (não da sessão) e responde sem login. Um anônimo lista o plano de contas de qualquer escritório passando id_empresa=1. Falha de autenticação + quebra de isolamento multi-tenant no mesmo endpoint.

Evidência: AjaxLeituraJsonContractTest::testListarPlanoContasRespondeSemSessao · documentado no Dédalo (sso-autenticacao.md).

S4 · Session poisoning sem guard nenhum

sgr/advogados/RequiredData.php

O endpoint é, na prática, session_start(); $_SESSION['uf'] = $_REQUEST['uf']; — sem Conf.php, sem auth, sem escopo de tenant. Qualquer anônimo grava na sessão a partir de input cru e recebe 200. Alto uso em produção (535 req em 4min).

Evidência: AutocompleteInfraContractTest.

S5 · Vazamento de SQL cru sem sessão

sgr/advogados/scripts/agenda/api/agenda_lista.php

Sem sessão, id_empresa fica vazio e o endpoint não respeita o envelope JSON — imprime o erro SQL cru ("Erro na consulta de contagem: You have an error in your SQL syntax...") com HTTP 200, vazando estrutura de query. check_copilot_access também ecoa id_empresa + nome do banco no corpo (info disclosure).

Evidência: AgendaContractTest, AutocompleteInfraContractTest.

S6 · Vazamento cross-tenant confirmado (dado real) 🔥

sgr/advogados/scripts/despesas/api/busca_quantidade_despesas_recorrentes.php

id_empresa do POST, sem sessão e sem validar tenant. Um anônimo informando id_empresa=1 recebeu a contagem real de despesas recorrentes do tenant 1 (EasyJur administrativa): ~1894. É a prova viva do padrão do S1 — lá o escritório de teste estava vazio e devolvia lista vazia; aqui vazou número real de produção.

Evidência: PessoasFinanceiroContractTest.

S7 · SQL injection explorável 🔥🔥 (o mais grave)

sgr/advogados/autoCompleteCidades.php — param term

O term é interpolado cru em WHERE nome LIKE '%$q%' … LIMIT 20, sem prepared statement nem escape. Error-based: term=aba'Fatal error no corpo (vaza caminho de arquivo). Exfiltração (boolean/comment): term=zzz%' or 1=1 -- retornou 5.570 linhas (vs as 20 do LIMIT) — a injeção fecha o LIKE, força condição verdadeira e comenta o LIMIT. tb_cidades é global (sem tenant). SQLi em série confirmada: o autoComplete.php genérico também é explorável (oráculo via "QA Bot"); autoCompleteCliente, autoCompleteProcessoPasta e getClienteTo_CkEDITOR2 interpolam input cru — vulneráveis no código, contidos só por falta de dados/or die. Raiz: queries não-preparadas (MySQLi). Fix: prepared statements em todos.

Evidência: SegurancaInjecaoTest (payloads de detecção não-destrutivos).

S8 · XSS refletido no CkEDITOR de processo

scripts_ajax/ClienteTo_CkEDITOR/getProcessoTo_CkEDITOR.php — param texto_final

No ramo de id_processo vazio, texto_final é ecoado cru com echo, sem htmlspecialchars. Payload <script>…</script> volta literal. Mitigado por exigir POST + sessão, mas a reflexão é total.

Evidência: SegurancaInjecaoTest.

S2 · Erro de auth da API com HTTP 200

api/<recurso>/<recurso>.php → auth.php

Sem Bearer, faz die('Token de invalido') em texto plano com HTTP 200 (deveria ser 401 JSON). Cliente não consegue distinguir sucesso de falha de auth pelo status.

Evidência: ApiListContractTest, ExtraLeituraContractTest.

S3 · Endpoints AJAX soltos sem guard de sessão

ajax_busca_agendamentos · ajax_leitura_cnj · busca_trecho_documentos · ajax_validar_limite_usuarios · agenda/ajax_is_workflow_negativo · agenda/ajax_verifica_hora_inicio · …

~9 endpoints respondem para anônimo (HTTP 200). Autocompletes têm guard inconsistente (uns incluem conf_db.min.php e barram, outros Conf.php e não). Nota: get_api_launch_status/get_aceite_status têm guard CORRETO (401) — é o padrão a seguir.

Evidência: caracterização em 6 arquivos de contrato.

S9 · Gap de guard se estende a ESCRITA + gateway central 🔥🔥

SCRIPTS_MYSQL/mysql_manager.php · agenda/workflow_tarefas/ajax_atualiza_prazos.php · chat/chats/ajax_cria_chat.php · popup/api/verifica_exibicao_popup.php · grupos/modal/ajax_modal_grupos.php · processos/ajax_processos_lista.php

O padrão do S3 atinge endpoints de escrita de alto tráfego. Pior de todos: mysql_manager.php (62 hits) é um gateway central de escrita que roteia para ~26 tabelas via REQUISITION[tabela][finalidade][campos], só inclui Conf.php (não autentica), e lê id_empresa dos campos do POST — não da sessão. Anônimo poderia escrever em qualquer tenant (mesma raiz de S1/S6, agora num gateway de escrita universal). Também sem guard: ajax_atualiza_prazos (UPDATE de prazos, core), ajax_cria_chat, e leitura em popup/grupos/processos_lista (secure_page_premium NÃO é guard de auth). Contidos hoje só por id_empresa vazio — proteção acidental.

Evidência: MysqlManagerContractTest, AtualizaPrazosContractTest, CriaChatContractTest, PopupContractTest, GruposModalContractTest, ProcessosListaContractTest.

S10 · XSS refletido em ajax_modal_grupos

sgr/advogados/scripts/grupos/modal/ajax_modal_grupos.php

modulo_origem é ecoado CRU no id da tabela e em blocos <script> do rodapé, sem htmlspecialchars. Payload "><script>alert(1)</script> volta literal.

Evidência: tests/php/http/GruposModalContractTest.php.

S11 · SQL injection SISTÊMICO na família autoComplete* 🔥🔥🔥

autoCompleteCidadeNome · autoCompleteComarcaJanela · autoCompleteBancos · autoCompleteCNPJ · autoCompleteTribunal · autoCompleteUsuarios · autoCompleteClienteJson · escritorios/autocomplete/* · autoCompleteTeste · … (35 de 60)

Varredura dos 87 autoComplete*.php: 35 dos 60 não-testados interpolam input cru (term/$_GET) em SQL sem prepared statement — mesma raiz do S7, sistêmica. 11 novos confirmados exploráveis via curl (error/boolean-based): exfil de tabela global inteira (Cidade/Comarca/Bancos 237/CNPJ 625/Tribunal 899/Usuarios), cross-tenant em autoCompleteClienteJson (0) OR 1=1 -- vazou 6831 pessoas de todos os tenants), e escritorios/* com auth gap (anônimo lê 192 emails @easyjur.com / lista de escritórios sem filtro de tenant). Bônus: autoCompleteTeste tem echo $sql; em produção (vaza a query + tenant em toda chamada). Os ~24 restantes têm o mesmo código vulnerável, contidos só por falta de dado no tenant QA.

Fix único: prepared statements em toda a família. Evidência: AutocompletesSqliSweep{A,B,C}Test.php.

🟠 Bugs de lógica

B1 · parse_percentual_retencao come a parte inteira

asaas/notas — parse_percentual_retencao()

'1.50' retorna '0,50%' — zera o dígito inteiro. É cálculo de retenção fiscal; impacto financeiro direto.

Evidência: ScriptsFunctionsExtraTest.

B2 · converteCampoTexto gruda palavras

timesheet/utils — converteCampoTexto()

Remove \n sem inserir espaço: "Texto\ncom quebra" vira "Textocom quebra".

Evidência: ScriptsFunctionsTest.

B3 · data_inicio_agenda — prazo errado no deslocamento negativo (confirmado)

sgr/advogados/scripts/subtipos/utils/functions.php:106

A linha 110 converte a data d/m/Y→ISO, mas a linha 112 a sobrescreve com o original. Então strtotime('10/06/2024') é lido como m/d/Y (6 de outubro) e o deslocamento opera na data errada. Ex.: data_inicio_agenda(-1,…,'10/06/2024')2024-10-04 (esperado 2024-06-07). Cálculo de prazo (core) errado em subprazo negativo; linhas 109-110 são código morto.

Evidência: PrazosNegocioTest (fixa o bug + marca o valor correto). A família calcular_prazo foi validada e está correta.

B4 · Busca de cidade falha por mojibake

sgr/advogados/scripts/autoCompleteCidades.php

?term=sao retorna null: tb_cidades está em mojibake latin1, então o LIKE após tirarAcentos não casa "São Paulo".

Evidência: relatado em AutocompleteContractTest.

B7 · Nome de plano de contas com hífen é truncado (JS)

sgr/advogados/scripts/plano_contas_utils.js

extrair_nome_plano_contas usa split('-')[1] em vez de separar só no primeiro ' - '. Nome que contém hífen é cortado: '1.1.01 - Receitas - Diversas''Receitas' (perde ' - Diversas'). Primeiro achado da camada de testes JS.

Evidência: tests/js/plano-contas-utils.test.js.

🟡 Robustez / contrato de API

R1 · HTTP sempre 200, mesmo em erro

Código de erro vai só no corpo (statusCode:'400'). Quebra a semântica REST — clientes não detectam erro pelo status.

R2 · Contratos inconsistentes entre endpoints irmãos

/api/user/list.php estoura 500 enquanto agenda/processo/andamento dão 400. api/*/show.php com id "perigoso" → 500. ajax_busca_processos sem sessão → Fatal TypeError servido como HTML dentro de um 200. ajax_verifica_consumo_ged sem sessão → Fatal (toca tb_notificacao_discord inexistente).

R3 · Shape e content-type inconsistentes

Vazio retorna ora [], ora null (autocompletes, select2_modelos). Content-Type text/html mesmo com corpo JSON na maioria (só cache-status declara application/json).

R4 · Uncaught TypeError vaza stack trace em HTTP 200

mysql_manager.php · processos/ajax_processos_lista.php · chat/chats/ajax_cria_chat.php

Recorrente: um TypeError (que é Error, não Exception) escapa do catch (Exception) do endpoint e vira Fatal não tratado, servido com caminho absoluto + stack trace no corpo e HTTP 200 (independe de display_errors, que está Off). Mesma classe do R2 (info disclosure + status errado).

Evidência: MysqlManagerContractTest, ProcessosListaContractTest, CriaChatContractTest.

⚪ Comportamentos anômalos / dívida

C1 · Abrir "+ Novo" grava rascunho no banco

Receita e agenda inserem linha (status='N') só por abrir o modal → lixo acumulado a cada abertura.

C2 · Agenda: soft-delete + busca não filtra status

Exclusão faz UPDATE status='D'; a busca da listagem não filtra status e ainda cacheia o SQL na sessão → item "apagado" pode reaparecer.

C3 · Funções duplicadas entre módulos

parse_colunas_excel, arrayMontaCheckboxCamposPersonalizados e outras existem em módulos diferentes com implementações divergentes → risco de divergência silenciosa.

C4 · Modal de publicações: aba "Principal" rouba o foco

Ao abrir #VISUALIZAR_publicacoes, a aba Principal carrega async e ao concluir reaplica .current em si mesma — um clique rápido em outra aba pode ser desfeito.