Skip to content

feat(seguranca): OWASP API Top 10 audit and vulnerability fixes#14

Merged
diego64 merged 1 commit into
mainfrom
feature/fase-9-seguranca
Jun 4, 2026
Merged

feat(seguranca): OWASP API Top 10 audit and vulnerability fixes#14
diego64 merged 1 commit into
mainfrom
feature/fase-9-seguranca

Conversation

@diego64

@diego64 diego64 commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Summary

Full security audit against OWASP API Top 10 (2023). Four vulnerabilities fixed (1 Critical, 2 High, 1 Medium). All 296 unit tests pass, lint and typecheck clean.

Findings & Fixes

CRITICAL — JWT signed with empty secret

File: src/infra/http/aplicacao.ts

secret: process.env.JWT_SECRETO ?? '' allowed the server to start with an empty JWT secret when the env var was missing. An attacker who knows this state can sign any JWT payload offline with an empty secret and gain full authenticated access — including admin routes. servidor.ts validates env at startup but criarAplicacao() is an exported function callable without that guard (tests, scripts, future entrypoints).

Fix: criarAplicacao() now throws immediately if JWT_SECRETO is undefined or shorter than 32 characters, before the JWT plugin is registered.


HIGH — PATCH /usuarios/:id params not validated by Zod

File: src/modulos/usuarios/apresentacao/rotas/rotas-usuario.ts

The :id param was read as req.params as { id: string } with no schema. Arbitrary strings reached the use case and the MongoDB driver. PATCH /visitantes/:id already had z.string().length(24) — inconsistent security posture between modules.

Fix: Added params: z.object({ id: z.string().length(24, 'ID deve ter 24 caracteres.') }) to the route schema. Removed the unsafe as cast; type now inferred from Zod.


HIGH — ReDoS via unescaped $regex input (MongoDB)

File: src/modulos/visitantes/infra/repositorios/repositorio-visitante-mongo.ts

nomeCompleto from the query string was passed directly as a MongoDB $regex pattern without escaping regex metacharacters. An authenticated attacker could send (a+)+$ or similar patterns causing catastrophic backtracking in MongoDB's regex engine, blocking the server thread for exponential time — a DoS achievable by a single authenticated user. Affected routes: GET /visitantes/hoje and GET /visitantes.

Fix: Added escaparRegex(texto: string): string that escapes all regex metacharacters (.*+?^${}()|[\]) using replace(/[.*+?^${}()|[\]\\]/g, '\\$&'). Applied in both $regex usages.


MEDIUM — Password hash compared with === (timing attack)

File: src/compartilhado/utilitarios/criptografia.ts

derivado.toString('hex') === hashArmazenado used a JavaScript string comparison, which is not constant-time. An attacker with precise latency measurement (e.g., on an internal network) could infer how many bytes of the correct hash match, enabling a guided brute-force.

Fix: Replaced with crypto.timingSafeEqual(derivado, Buffer.from(hashArmazenado, 'hex')) after a length-equality check (different sizes would throw in timingSafeEqual).


LOW — /saude endpoint exposed NODE_ENV

File: src/infra/http/aplicacao.ts

GET /saude returned { status: 'ok', ambiente: 'production' }. Confirming the environment aids fingerprinting (attacker learns that Swagger isn't reachable, which env-specific behaviors apply, etc.).

Fix: Removed the ambiente field. Response is now { sucesso: true, dados: { status: 'ok' } }.


LOW — CPF logged in full via query string

File: src/infra/http/aplicacao.ts

GET /visitantes?cpf=12345678900 logged the full URL in onRequest/onResponse hooks. CPF is a sensitive personal data field (LGPD Art. 5, II). Anyone with Grafana/Loki access could extract CPFs of all queried visitors.

Fix: Added sanitizarUrl(url: string): string that replaces the cpf query param value with [OMITIDO] before logging.

OWASP API Top 10 — Status

# Vulnerability Status
API1 BOLA ✅ No BOLA — ownership enforced in domain layer
API2 Broken Authentication ✅ Fixed (empty JWT secret, timingSafeEqual)
API3 Broken Object Property Level Auth ✅ No mass assignment; senhaHash never returned
API4 Unrestricted Resource Consumption ✅ Global rate limit + per-route limits on login/refresh
API5 BFLA ✅ Admin routes protected by autorizarAdmin
API6 Sensitive Business Flows ✅ Time/day/CPF-per-day restrictions in domain
API7 SSRF ✅ No outbound HTTP calls with user-supplied URLs
API8 Security Misconfiguration ✅ Helmet, explicit CORS, Swagger gated to dev only
API9 Improper Inventory Management ✅ No undocumented or orphan routes
API10 Unsafe API Consumption ✅ No third-party API calls

Remaining backlog (non-blocking)

  • src/testes/auxiliares/auxiliar-bd.ts:10 — hardcoded MongoDB credentials as fallback; should throw if MONGO_URI is not set
  • docker-compose.yml — ports 27017 and 6379 exposed on host; should be internal-only for staging/production
  • Refresh token cookie Secure flag — evaluate enabling for staging environments with TLS

Test plan

pnpm lint       # ✅ no errors
pnpm typecheck  # ✅ no errors
pnpm exec vitest run src/testes/unitarios  # ✅ 296 tests, 28 files

🤖 Generated with Claude Code

@diego64 diego64 merged commit 9676e6e into main Jun 4, 2026
1 check passed
@diego64 diego64 deleted the feature/fase-9-seguranca branch June 4, 2026 19:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant