| name | pentest-backend-nestjs |
| description | Pentest de backend NestJS — OWASP API Security Top 10 (2023).
Cobre BOLA/IDOR, autenticação quebrada, mass assignment, SSRF, prototype pollution,
injeção SQL/NoSQL, RCE via upload, race conditions (TOCTOU), JWT alg confusion,
rate-limit, CORS, helmet, lógica de negócio e autorização por recurso.
TRIGGERS: pentest backend, pentest nestjs, pentest api, api security, owasp api, bola, idor, mass assignment, ssrf, prototype pollution, jwt alg none, rate limit, cors misconfig, nestjs guards, nestjs security
|
🛡️ pentest-backend-nestjs — Auditoria Ofensiva de NestJS
Base normativa: OWASP API Security Top 10 (2023) + CWE Top 25 + NIST SP 800-95.
Required Reading
apps/api/src/main.ts (helmet, cors, rate-limit, pipes)
apps/api/src/app.module.ts
libs/**/data-source/src/lib/controllers/*.ts
libs/**/data-source/src/lib/services/*.ts
libs/**/data-source/src/lib/entities/*.ts
.env* (nunca commit), ormconfig.*, prisma/schema.prisma
API1:2023 — Broken Object Level Authorization (BOLA/IDOR)
O bug mais comum e mais lucrativo em APIs.
rg -n "@(Get|Put|Patch|Delete|Post)\(.*:id" libs/ apps/ -A 10 | rg -n "ownership|belongsTo|userId.*==.*@Req|req\.user"
Padrões a caçar:
GET /users/:id que retorna dados sem verificar req.user.id === id ou papel admin
PATCH /orders/:id sem checar order.userId === req.user.id
DELETE /posts/:id sem verificar ownership
- Recursos aninhados:
/users/:userId/documents/:docId sem verificar que doc.userId === userId
PoC:
TOKEN_A=$(curl -s -X POST http://localhost:3000/auth/login -d '{"email":"a@x.com","password":"..."}' -H 'Content-Type: application/json' | jq -r .token)
curl -H "Authorization: Bearer $TOKEN_A" http://localhost:3000/users/2
Fix:
@Get(':id')
async findOne(@Param('id') id: string, @CurrentUser() user: UserDto) {
const target = await this.service.findById(id);
if (target.id !== user.id && !user.roles.includes('admin')) {
throw new ForbiddenException();
}
return target;
}
Ou melhor: CASL/accesscontrol com policies centralizadas.
API2:2023 — Broken Authentication
JWT
rg -n "jwt\.verify|jwt\.decode|JwtModule" libs/ apps/ -A 5
rg -n "algorithms:\s*\[" libs/ apps/ -A 2
Checklist:
Login/Senha
Sessão
API3:2023 — Broken Object Property Level Authorization
Mass Assignment + Excessive Data Exposure.
Mass assignment
rg -n "ValidationPipe" apps/api/src/main.ts -A 10
rg -n "whitelist:\s*true" apps/ libs/
rg -n "forbidNonWhitelisted:\s*true" apps/ libs/
Checklist:
PoC:
curl -X PATCH http://localhost:3000/users/me \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"x","role":"admin","isEmailVerified":true}'
Excessive Data Exposure
rg -n "SELECT \*|find\(\{\}" libs/ apps/
rg -n "\.find\(|\.findOne\(" libs/ apps/ -A 3 | rg "select:"
API4:2023 — Unrestricted Resource Consumption
rg -n "ThrottlerModule|@Throttle\(" libs/ apps/
rg -n "json\(\{\s*limit" apps/
rg -n "RegExp\(.*req\.|new RegExp\(.*query" libs/ apps/
API5:2023 — Broken Function Level Authorization
rg -n "@UseGuards\(" libs/ apps/ | wc -l
rg -n "@Controller\(" libs/ apps/ | wc -l
rg -n "@Roles\(|@Permissions\(" libs/ apps/
API6:2023 — Unrestricted Access to Sensitive Business Flows
Lógica de negócio que permite abuso em massa:
API7:2023 — Server Side Request Forgery (SSRF)
rg -n "axios\.(get|post|request)\(" libs/ apps/ -A 3 | rg "req\.|body\.|query\."
rg -n "fetch\(.*(req\.|body\.|params\.)" libs/ apps/
rg -n "http\.get\(|request\(" libs/ apps/
Cenários típicos:
- Webhook endpoint aceita URL do usuário
- Image proxy / PDF generator
- OAuth callback URL sem allowlist
Allowlist correta:
const ALLOWED_HOSTS = new Set(['api.parceiro.com']);
const url = new URL(input);
if (!ALLOWED_HOSTS.has(url.hostname)) throw new ForbiddenException();
PoC:
curl -X POST http://localhost:3000/webhook -d '{"url":"http://169.254.169.254/latest/meta-data/iam/security-credentials/"}'
API8:2023 — Security Misconfiguration
Helmet / CORS / Cookies
rg -n "app\.use\(helmet" apps/api/src/main.ts
rg -n "enableCors|CorsOptions" apps/api/src/main.ts -A 8
rg -n "cookie-parser|session" apps/
Error handling
rg -n "stack\s*:|toString\(\)" libs/ apps/ -A 2 | rg -i "res\.|json\("
API9:2023 — Improper Inventory Management
API10:2023 — Unsafe Consumption of APIs
rg -n "rejectUnauthorized:\s*false" libs/ apps/
SQL / NoSQL Injection
TypeORM
rg -n "createQueryBuilder|query\(" libs/ apps/ -A 3 | rg -v "parameters:|\\?"
rg -n "\.query\(.*\$\{|\.query\(.*\+\s*" libs/ apps/
Prisma
- Prisma é seguro por padrão, mas
$queryRawUnsafe é um anti-pattern.
rg -n "\$queryRawUnsafe|\$executeRawUnsafe" libs/ apps/
MongoDB
rg -n "\.find\(.*req\." libs/ apps/ -A 2
Prototype Pollution
rg -n "lodash\.merge|_\.merge|object-assign-deep|deepmerge" libs/ apps/
rg -n "Object\.assign\(.*req\.body" libs/ apps/
RCE via Upload / Dependência / Eval
rg -n "FileInterceptor|multer" libs/ apps/ -A 10
rg -n "eval\(|new Function\(|require\(.*req\." libs/ apps/
rg -n "child_process|exec\(|execSync\(|spawn\(.*(req|body|query)" libs/ apps/
Race Conditions / TOCTOU
Cenário clássico: saldo, cupons, estoque.
const user = await repo.findOne(id);
if (user.balance >= amount) {
user.balance -= amount;
await repo.save(user);
}
await repo.decrement({ id, balance: MoreThanOrEqual(amount) }, 'balance', amount);
Procurar:
rg -n "findOne.*\n.*save\(" libs/ apps/ -U
rg -n "@VersionColumn|optimistic|pessimistic" libs/ apps/
Logging & Observabilidade
rg -n "console\.log\(.*password|console\.log\(.*token" libs/ apps/
Headers & Hardening Produção
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Content-Security-Policy mesmo em API (protege endpoints HTML de docs)
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Output Snippet
### 🛡️ Backend (NestJS) — N findings
| ID | Sev | OWASP API | CWE | File:Line | Issue |
|----|---|---|---|---|---|
| B-001 | 🔴 Critical | API1:2023 | CWE-639 | libs/user/.../user.controller.ts:42 | IDOR em GET /users/:id |
| B-002 | 🔴 Critical | API3:2023 | CWE-915 | libs/user/.../update-user.dto.ts | Mass assignment de `role` |
| B-003 | 🟠 High | API7:2023 | CWE-918 | libs/webhook/.../webhook.service.ts:18 | SSRF em axios.get(req.body.url) |
Anti-patterns a SEMPRE reportar
- DTO sem
@IsXxx + ValidationPipe sem whitelist:true
- Controller de recurso sem ownership check
jwt.verify sem algorithms fixo
cors({ origin: '*', credentials: true }) (inconsistente, browsers ignoram mas config é bug)
bcrypt.compare fora de função constant-time para username enumeration
rejectUnauthorized: false em axios
$queryRawUnsafe / string concat em SQL
process.env.X lido sem validação (@nestjs/config com schema Joi/Zod)
- Swagger aberto em prod sem auth
- Stack trace vazando em
ExceptionFilter