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.
⚠ Levar pro time agora
- S7 — SQL injection EXPLORÁVEL em
autoCompleteCidades.php:term=zzz%' or 1=1 --retornou 5.570 linhas (vs 20). Exfiltração confirmada, sem prepared statement. - S9 — Gateway central de escrita
mysql_manager.php(62 hits) sem guard de sessão e lendoid_empresado POST → anônimo poderia escrever em ~26 tabelas de qualquer tenant (cross-tenant). Contido hoje só por proteção acidental. - S11 — SQL 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,autoCompleteClienteJsonvazou 6831 pessoas cross-tenant,escritorios/*vazam 192 emails a anônimo). Fix único: prepared statements na família toda. - 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. - S1 — Mesmo padrão no plano de contas (id_empresa via POST, sem login).
- S4 —
RequiredData.phpgrava na sessão a partir de input cru, sem auth (session poisoning). - B1 —
parse_percentual_retencaocome a parte inteira (1.50 → 0,50%) — cálculo fiscal. - B5 — Validação de CNPJ/CPF só checa comprimento, não o dígito verificador.
| # | Sev | Resumo | Status |
|---|---|---|---|
| S1 | crít | Plano de contas: anônimo lê de qualquer escritório (id_empresa via POST, sem login) | aberto |
| S2 | crít | /api/* sem Bearer → "Token de invalido" em texto + HTTP 200 (devia 401) | aberto |
| S3 | crít | ~9 ajax_*.php/autocomplete soltos respondem sem sessão | aberto |
| S4 | crít | RequiredData.php: session poisoning de input cru, zero guard | aberto |
| S5 | crít | api/agenda_lista.php sem sessão vaza fragmento de SQL no corpo | aberto |
| S6 | crít | Despesas recorrentes: anônimo leu contagem REAL do tenant 1 (~1894) — vazamento confirmado | aberto |
| S7 | crít | SQL injection explorável em autoCompleteCidades.php (term) — exfiltração confirmada (5.570 linhas) | aberto |
| S8 | crít | XSS refletido em getProcessoTo_CkEDITOR.php (texto_final ecoado sem escape) | aberto |
| S9 | crít | Gateway 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_lista | aberto |
| S10 | crít | XSS refletido em ajax_modal_grupos.php (modulo_origem ecoado cru) | aberto |
| S11 | crít | SQLi 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 |
| B1 | alto | parse_percentual_retencao: come parte inteira (fiscal) | aberto |
| B2 | alto | converteCampoTexto: remove quebra de linha sem espaço, gruda palavras | aberto |
| B3 | alto | data_inicio_agenda: código morto no deslocamento negativo | aberto |
| B4 | alto | Busca de cidade falha por mojibake em tb_cidades | aberto |
| B5 | alto | Validação CNPJ/CPF só checa comprimento, não o dígito verificador | aberto |
| B6 | alto | parse_format_number('1.234,56')→R$ 1,23; ajax_filter_adv chama ShowErro() inexistente | aberto |
| B7 | alto | JS extrair_nome_plano_contas: split('-') trunca nome com hífen ("Receitas - Diversas"→"Receitas") | aberto |
| R1 | méd | HTTP sempre 200, mesmo em erro (código só no corpo) | aberto |
| R2 | méd | Contratos inconsistentes: 500 vs 400, Fatal servido como HTML em 200 | aberto |
| R3 | méd | Shape no vazio ([] vs null) e content-type text/html em JSON | aberto |
| R4 | méd | Uncaught TypeError escapa do catch(Exception) → stack trace + path em HTTP 200 (mysql_manager, processos_lista, cria_chat) | aberto |
| C1 | dív | Abrir modal "+ Novo" já grava rascunho no banco | aberto |
| C2 | dív | Agenda: exclusão soft-delete + busca não filtra status (mostra apagado) | aberto |
| C3 | dív | Funções duplicadas entre módulos com implementações divergentes | aberto |
| C4 | dív | Modal de publicações: aba "Principal" rouba o foco após carregar | aberto |
🔴 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.phpLê 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.phpO 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.phpSem 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.phpLê 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 — paramterm
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 — paramtexto_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.phpSem 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.phpO 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.phpmodulo_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:106A 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.jsextrair_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.phpRecorrente: 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.