feat(seguranca): OWASP API Top 10 audit and vulnerability fixes#14
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.tssecret: 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.tsvalidates env at startup butcriarAplicacao()is an exported function callable without that guard (tests, scripts, future entrypoints).Fix:
criarAplicacao()now throws immediately ifJWT_SECRETOis undefined or shorter than 32 characters, before the JWT plugin is registered.HIGH —
PATCH /usuarios/:idparams not validated by ZodFile:
src/modulos/usuarios/apresentacao/rotas/rotas-usuario.tsThe
:idparam was read asreq.params as { id: string }with no schema. Arbitrary strings reached the use case and the MongoDB driver.PATCH /visitantes/:idalready hadz.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 unsafeascast; type now inferred from Zod.HIGH — ReDoS via unescaped
$regexinput (MongoDB)File:
src/modulos/visitantes/infra/repositorios/repositorio-visitante-mongo.tsnomeCompletofrom the query string was passed directly as a MongoDB$regexpattern 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/hojeandGET /visitantes.Fix: Added
escaparRegex(texto: string): stringthat escapes all regex metacharacters (.*+?^${}()|[\]) usingreplace(/[.*+?^${}()|[\]\\]/g, '\\$&'). Applied in both$regexusages.MEDIUM — Password hash compared with
===(timing attack)File:
src/compartilhado/utilitarios/criptografia.tsderivado.toString('hex') === hashArmazenadoused 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 intimingSafeEqual).LOW —
/saudeendpoint exposedNODE_ENVFile:
src/infra/http/aplicacao.tsGET /saudereturned{ 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
ambientefield. Response is now{ sucesso: true, dados: { status: 'ok' } }.LOW — CPF logged in full via query string
File:
src/infra/http/aplicacao.tsGET /visitantes?cpf=12345678900logged the full URL inonRequest/onResponsehooks. 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): stringthat replaces thecpfquery param value with[OMITIDO]before logging.OWASP API Top 10 — Status
senhaHashnever returnedautorizarAdminRemaining backlog (non-blocking)
src/testes/auxiliares/auxiliar-bd.ts:10— hardcoded MongoDB credentials as fallback; should throw ifMONGO_URIis not setdocker-compose.yml— ports 27017 and 6379 exposed on host; should be internal-only for staging/productionSecureflag — evaluate enabling for staging environments with TLSTest plan
🤖 Generated with Claude Code