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