- dialplan_extensions table (tenant-scoped, RLS): structured editor per agente.md secao 43 -- context, condition field/expr, actions/anti-actions (JSON), continue, order, enabled. One condition per extension (deliberate simplification vs raw FreeSWITCH's multi-condition extensions). - dialplan_versions table (tenant-scoped, RLS): generate/validate/version/ activate flow (secao 44). Reactivating an older version IS the rollback mechanism -- no separate endpoint needed. - apps/api/src/dialplan: extensions CRUD + versions/generate (builds XML, validates well-formedness with fast-xml-parser, saves as DRAFT) + versions/:id/activate (atomically flips ACTIVE, supersedes the previous one). Reused freeswitch.view/.configure permissions rather than inventing new ones not in the agente.md permission list. - packages/telephony: buildDialplanXml() plus ALLOWED_DIALPLAN_APPLICATIONS, an explicit allowlist (answer/bridge/playback/hangup/set/export/... -- deliberately no system/exec/socket) guarding against a tenant configuring a dialplan action that runs arbitrary commands on the FreeSWITCH host (agente.md secao 180) - b2bcall-fs-config resolves dialplan dynamically per call (unlike Trunks' file+rescan approach -- dialplan is fetched fresh via mod_xml_curl on every call anyway) by tenant id from the variable_b2bcall_tenant_id channel variable already injected at directory resolution, then serving whichever DialplanVersion is ACTIVE for that context - verified end-to-end: created a rule for destination_number 7000, generated and activated v1, originated a call that actually routed through the dialplan (not bypassing it via &app()) -- CALL_CREATED -> CALL_ANSWERED -> CALL_ENDED with the correct tenantId throughout. Created and activated a v2, then rolled back to v1 by reactivating it; status transitions (ACTIVE/SUPERSEDED) all confirmed via the API. CRITICAL FINDING, fixed in this same phase: deliberately testing that the application allowlist rejects 'system' got back 201 instead of 400 -- NestJS's ValidationPipe had been silently inert across all of apps/api's @Body() DTOs since the API was first created. Root cause: running via (esbuild) instead of a real build -- esbuild doesn't always resolve cross-file parameter types for design:paramtypes metadata, and Nest skips validation without any error when it can't determine the DTO class. Fixed by always building with tsc before running (tsc && tsx dist/main.js -- still via tsx because internal workspace packages aren't built to JS yet). Re-verified with two deliberate bad-input tests post-fix, both correctly rejected with 400. A stray malicious test row (dialplan action 'system') created while the bug was live was deleted; it was never baked into an activated version, so nothing could have executed it. See docs/VALIDATION_PIPE_BUG.md for the full writeup. docs/DIALPLAN.md, docs/VALIDATION_PIPE_BUG.md, docs/EXTENSIONS.md updated
76 lines
3.6 KiB
Markdown
76 lines
3.6 KiB
Markdown
# Achado crítico: validação de entrada não funcionava sob `tsx`
|
|
|
|
**Resumo**: de quando `apps/api` foi criada (fase apps/api) até a fase Dialplan,
|
|
`ValidationPipe` global do NestJS **não validava nada**. Todo `@Body()`
|
|
passava batendo, incluindo campos não permitidos (`forbidNonWhitelisted`) e
|
|
valores fora de qualquer allowlist (`@IsIn`). Descoberto ao testar
|
|
deliberadamente que a allowlist de applications do dialplan (que existe
|
|
especificamente pra impedir uma tenant de configurar `application: "system"`
|
|
com dados de shell arbitrários — agente.md secao 180) **aceitou** a entrada
|
|
maliciosa com 201.
|
|
|
|
## Causa raiz
|
|
|
|
`apps/api` rodava via `tsx src/main.ts` (esbuild por baixo). O NestJS decide
|
|
qual classe usar pra instanciar/validar um `@Body()` lendo a metadata
|
|
`design:paramtypes` do método do controller (reflect-metadata,
|
|
`emitDecoratorMetadata` do TypeScript). **esbuild não faz checagem de tipos
|
|
completa entre arquivos** — pra parâmetros cujo tipo vem de um `import` de
|
|
outro arquivo, ele às vezes emite `Object` genérico em vez da classe real.
|
|
Quando `ValidationPipe` recebe `metatype === Object`, ele **pula a validação
|
|
silenciosamente** (comportamento documentado do Nest: tipos primitivos/
|
|
genéricos são considerados "nada pra validar").
|
|
|
|
Isso é a mesma classe de bug já encontrada na fase Extensions
|
|
(`PermissionGuard` injetando `Reflector` via construtor chegava `undefined`
|
|
em runtime) — mas ali o sintoma era um crash óbvio (`TypeError`), fácil de
|
|
notar. Aqui o sintoma é **ausência de erro**: a validação simplesmente não
|
|
roda, sem log, sem exceção, sem pista nenhuma a não ser testar
|
|
deliberadamente com entrada inválida.
|
|
|
|
## Correção
|
|
|
|
`tsc` de verdade faz checagem de tipos completa e emite `design:paramtypes`
|
|
corretamente (confirmado inspecionando o `dist/*.js` gerado: aparece a
|
|
referência real da classe, ex. `create_extension_dto_1.CreateExtensionDto`,
|
|
em vez de `Object`). A partir de agora `apps/api` **sempre** builda com
|
|
`tsc` antes de rodar:
|
|
|
|
```json
|
|
"build": "tsc -p tsconfig.json",
|
|
"start": "tsx dist/main.js",
|
|
"dev": "tsc -p tsconfig.json && tsx dist/main.js"
|
|
```
|
|
|
|
Importante: o `start`/`dev` rodam o JS compilado através do **`tsx`**, não
|
|
do `node` puro — porque os pacotes internos (`@b2bcall/shared`,
|
|
`@b2bcall/database`, ...) ainda não têm build próprio (`main` aponta pro
|
|
`.ts` fonte, não pra um `dist/`). `tsx` resolve isso via seu loader; `node`
|
|
sozinho quebraria tentando importar um arquivo `.ts` diretamente. Ver
|
|
docs/AUTHENTICATION.md pra mais contexto sobre essa limitação dos pacotes
|
|
internos.
|
|
|
|
**Nunca rode `apps/api` com `tsx src/main.ts` (ou `tsx watch`) direto — só
|
|
via `pnpm run build && pnpm run start`, ou `pnpm run dev`.**
|
|
|
|
## Limpeza feita
|
|
|
|
Uma extension de dialplan de teste com `application: "system"` foi criada
|
|
durante o teste que expôs o bug (nunca chegou a ser incluída numa versão
|
|
gerada/ativada — não havia como executar de verdade) e foi apagada
|
|
imediatamente após a correção.
|
|
|
|
## O que revisar
|
|
|
|
- `packages/telephony`, `packages/auth`, `packages/database`: não usam
|
|
`ValidationPipe`/decorators do Nest, não são afetados por esse bug
|
|
específico.
|
|
- `apps/freeswitch-events` e `apps/freeswitch-config`: não usam NestJS, não
|
|
são afetados.
|
|
- Todos os DTOs de `apps/api` (auth, extensions, trunks, dialplan) — a
|
|
correção é geral (troca de runtime, não patch por endpoint), então todos
|
|
passam a validar corretamente a partir desta fase. Reverificado com dois
|
|
testes deliberados pós-correção (campo não permitido em `/extensions`,
|
|
application fora da allowlist em `/dialplan/extensions`) — ambos
|
|
corretamente rejeitados com 400.
|