Secara default, NestJS mengembalikan bentuk respons error yang berbeda untuk error validasi, pengecualian HTTP, dan pengecualian yang tidak tertangani — yang dapat membocorkan stack trace, path internal, dan pesan error database mentah ke pemanggil. Sebuah kasus nyata dari audit klien menunjukkan objek error Prisma dikembalikan apa adanya, mengekspos query SQL yang gagal, nama field skema, bahkan hostname database. ExceptionFilter global harus menangkap semua pengecualian yang tidak tertangani, mencatat detail lengkap di sisi server, dan hanya mengembalikan respons generik ke klien.
Artikel ini merekomendasikan batas global (100 permintaan/menit per IP), batas ketat untuk endpoint auth (10 permintaan/menit per IP), dan batas lebih tinggi untuk pengguna terautentikasi (500 permintaan/menit per pengguna). Redis diperlukan untuk menyimpan penghitung rate limit karena penyimpanan in-memory default tidak berfungsi dengan beberapa instans API — skala horizontal akan membuat penyerang bisa melewati batas dengan menyerang instans yang berbeda.
Autentikasi (memverifikasi siapa pemanggil) ditangani oleh JWT guard yang memvalidasi token dan melampirkan pengguna ke konteks permintaan, sementara otorisasi (menentukan apa yang bisa dilakukan pemanggil) ditangani oleh guard terpisah seperti RolesGuard dan ownership guard menggunakan dekorator seperti @Roles('admin') dan @CheckOwnership(). Memisahkan keduanya dalam guard berbeda memastikan otorisasi diterapkan secara sistematis di lapisan controller dan dapat diaudit, bukan tersebar tidak konsisten di dalam method service.
Peristiwa yang relevan dengan keamanan yang harus dicatat meliputi setiap upaya autentikasi (sukses atau gagal, dengan IP dan user agent), setiap kegagalan otorisasi, setiap tindakan admin beserta kondisi sebelum dan sesudahnya, serta setiap kegagalan validasi input. Log harus dikirim ke layanan terpusat seperti ELK stack, Loki, atau CloudWatch — menyimpan log di server yang sama dengan aplikasi berarti penyerang yang mendapatkan akses dapat menghapusnya. Artikel ini merekomendasikan menyimpan log keamanan minimal 90 hari, atau satu tahun untuk kepatuhan.
Mengaktifkan whitelist: true akan menghapus properti permintaan yang tidak dideklarasikan dalam DTO, sementara forbidNonWhitelisted: true melangkah lebih jauh dengan melempar error daripada diam-diam mengabaikan field yang tidak dikenal. Bersama-sama, dua opsi ini mencegah serangan mass assignment — di mana penyerang menambahkan field ekstra seperti isAdmin atau role dengan harapan aplikasi akan menyimpannya — dan memastikan hanya properti yang didefinisikan secara eksplisit yang mencapai lapisan service.
Membangun API yang berfungsi adalah taruhan meja. Membangun yang aman di bawah kondisi serangan dunia nyata adalah di mana sebagian besar sistem produksi gagal. Saya menjalankan API NestJS yang melayani data ERP untuk klien bisnis — catatan keuangan, data karyawan, level inventaris. Kegagalan keamanan API pada sistem ini memiliki konsekuensi bisnis langsung. Setelah mengamankan beberapa API NestJS dan berurusan dengan upaya serangan nyata (brute force, probe injeksi, serangan enumerasi), saya memiliki daftar periksa konfigurasi keamanan yang saya terapkan di setiap deployment.
Kontrol keamanan pertama dalam API NestJS mana pun adalah validasi input menggunakan class-validator dan class-transformer. Setiap DTO (Data Transfer Object) harus memiliki dekorator validasi eksplisit — @IsString(), @IsEmail(), @IsInt({ min: 0 }), @MaxLength(255), dll. Aktifkan ValidationPipe global dengan whitelist: true (menghapus properti yang tidak dikenal) dan forbidNonWhitelisted: true (melempar pada properti yang tidak dikenal daripada diam-diam mengabaikannya).
Konfigurasikan ValidationPipe global di main.ts dengan pengaturan ketat. Tambahkan Helmet untuk security header melalui paket @nestjs/helmet. Tambahkan middleware kompresi. Tetapkan batas ukuran body permintaan untuk mencegah serangan DoS payload besar. Empat baris konfigurasi ini — ValidationPipe, Helmet, kompresi, batas body — mencakup area permukaan signifikan kerentanan API umum.
// main.ts — NestJS production security configuration
import { NestFactory } from '@nestjs/core'
import { ValidationPipe } from '@nestjs/common'
import helmet from 'helmet'
import * as compression from 'compression'
import { AppModule } from './app.module'
import { HttpExceptionFilter } from './filters/http-exception.filter'
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn'], // no verbose in production
})
// 1. Security headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
}))
// 2. Input validation (global)
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // strip unknown properties
forbidNonWhitelisted: true, // throw on unknown properties
transform: true, // auto-transform to DTO types
disableErrorMessages: false, // keep validation messages
exceptionFactory: (errors) => {
// Return uniform error format
return new BadRequestException({
statusCode: 400,
error: 'Validation Error',
details: errors.map(e => ({
field: e.property,
constraints: Object.values(e.constraints ?? {}),
})),
})
},
}))
// 3. Body size limit (prevent large payload DoS)
app.use(compression())
// In Express underlying: app.use(express.json({ limit: '1mb' }))
// 4. Global exception filter (no stack traces in responses)
app.useGlobalFilters(new HttpExceptionFilter())
// 5. CORS (allowlist only)
const origins = process.env.ALLOWED_ORIGINS?.split(',') ?? []
app.enableCors({
origin: (origin, cb) => (!origin || origins.includes(origin)) ? cb(null, true) : cb(new Error('Not allowed')),
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
})
await app.listen(3000)
}
bootstrap()Dari pengalaman saya mengamankan API ERP: tambahkan ExceptionFilter kustom yang menormalkan respons error. Secara default, NestJS mengembalikan bentuk respons error yang berbeda untuk error validasi, pengecualian HTTP, dan pengecualian yang tidak tertangani. Format respons error yang seragam mencegah kebocoran informasi (stack trace, path internal, pesan error database) dan membuat pemindaian keamanan lebih sulit. Jangan pernah mengembalikan pesan error database mentah.
Setiap endpoint API publik memerlukan rate limiting. NestJS memiliki paket @nestjs/throttler yang berintegrasi bersih. Konfigurasikan tiga tingkat: batas global (100 permintaan per menit per IP), endpoint auth (10 permintaan per menit per IP — jauh lebih ketat), dan endpoint terautentikasi (500 permintaan per menit per pengguna). Simpan penghitung rate limit di Redis untuk skala horizontal.
Di tingkat infrastruktur, integrasikan dengan fail2ban untuk memblokir penyerang persisten di lapisan nginx sebelum mereka mencapai aplikasi NestJS Anda. Log JSON terstruktur dari API Anda (timestamp, IP, endpoint, status code, user_id) dan tulis filter fail2ban yang memicu pada pola seperti: 20+ kegagalan auth dari satu IP dalam 5 menit, atau 50+ error validasi 422 dari satu IP dalam 10 menit.
// throttler.module.ts — rate limiting with Redis storage
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'
import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis'
import { APP_GUARD } from '@nestjs/core'
@Module({
imports: [
ThrottlerModule.forRootAsync({
inject: [RedisService],
useFactory: (redis: RedisService) => ({
throttlers: [
{ name: 'global', ttl: 60000, limit: 100 }, // 100/min global
{ name: 'auth', ttl: 60000, limit: 10 }, // 10/min for auth
{ name: 'user', ttl: 60000, limit: 500 }, // 500/min per user
],
storage: new ThrottlerStorageRedisService(redis.client),
}),
}),
],
providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }],
})
export class ThrottlerConfigModule {}
// Apply stricter limit on auth endpoints
@Controller('auth')
@Throttle({ auth: { ttl: 60000, limit: 5 } }) // 5 attempts/min
export class AuthController {
@Post('login')
@UseGuards(ThrottleByIpGuard) // IP-based, not user-based
async login(@Body() dto: LoginDto, @Req() req: Request) {
// Track failed attempts for account lockout
const attempts = await this.redisService.incr(`login_attempts:${dto.email}`)
if (attempts > 10) throw new TooManyRequestsException('Account locked')
// ...
}
}
// fail2ban filter for nginx logs
// /etc/fail2ban/filter.d/nestjs-api.conf:
// [Definition]
// failregex = ^<HOST> .* "POST /auth/login HTTP.*" 401
// ^<HOST> .* ".*" 422 # validation failures
// ignoreregex =Guard NestJS adalah tempat yang tepat untuk mengimplementasikan autentikasi. JWT guard memvalidasi token di setiap permintaan dan melampirkan pengguna ke konteks permintaan. Kemudian gunakan dekorator kustom untuk otorisasi: @Roles('admin') diperiksa oleh RolesGuard, @CheckOwnership() diverifikasi oleh guard kepemilikan yang mengquery sumber daya. Aturan kritis: autentikasi (siapa Anda?) dan otorisasi (apa yang bisa Anda lakukan?) adalah masalah terpisah yang ditangani oleh guard terpisah.
Saya meninjau API NestJS produksi untuk klien yang mengembalikan objek error Prisma secara verbatim dalam respons API. Respons ini mencakup query SQL yang gagal, nama field skema database, dan dalam satu kasus string kesalahan koneksi yang menyebutkan hostname database. Semua ini terlihat oleh siapa pun yang mengirim permintaan yang salah bentuk. Penyerang mempelajari skema database, pola query, dan detail infrastruktur Anda secara gratis.
Peristiwa yang relevan dengan keamanan harus dicatat dengan konteks yang cukup untuk menyelidiki insiden. Gunakan library logging terstruktur (Winston atau Pino) dengan output JSON. Log: setiap peristiwa autentikasi (sukses/gagal, IP, user agent), setiap kegagalan otorisasi (siapa, sumber daya apa, mengapa ditolak), setiap tindakan admin, setiap kegagalan validasi input.
Setiap API NestJS produksi yang saya deploy mendapatkan: ValidationPipe global (whitelist, forbidNonWhitelisted, transform), Helmet security header, rate limiting (throttler + Redis), allowlist CORS, JWT authentication guard, RBAC authorization guard, ExceptionFilter global (tidak ada stack trace dalam respons), structured audit logging (Winston + JSON), pencegahan SQL injection (Prisma ORM, hanya raw query yang divalidasi), sanitasi input untuk XSS, dan endpoint health check yang tidak membocorkan informasi sistem.
Sumber & Bacaan Lanjutan