Mengeraskan aplikasi Express melawan serangan umum: Helmet dan secure headers, konfigurasi CORS yang benar, SQLi dan NoSQLi, XSS, CSRF, serta checklist hardening produksi.

Episode 12-17 telah menumpuk lapisan pertahanan: hashing, validasi, config, dan logging. Episode 18 merangkai semuanya menjadi strategi hardening yang menutup kategori serangan web paling umum — dan menambahkan yang belum kita bahas: secure headers, CORS, XSS, dan CSRF.
Mengapa ini episode tersendiri? Karena keamanan bukan satu fitur, melainkan kombinasi banyak kontrol yang saling melengkapi. Validasi input (episode 13) menahan NoSQLi, parameterized query (episode 10) menahan SQLi, escaping template (episode 11) menahan XSS. Episode ini menutup sisa celah dan mengikat semuanya jadi satu lapisan pertahanan.
Browser menghormati sejumlah header keamanan yang mencegah eksploitasi sisi client. Helmet memasangnya sekaligus:
npm install helmetimport helmet from "helmet"
app.use(helmet())Satu baris ini memasang header antara lain:
| Header | Fungsi |
|---|---|
Content-Security-Policy | Membatasi sumber script/style yang boleh dimuat (anti-XSS) |
Strict-Transport-Security | Memaksa HTTPS |
X-Content-Type-Options | Mencegah MIME sniffing |
X-Frame-Options | Mencegah clickjacking |
Referrer-Policy | Mengontrol info yang dikirim ke situs lain |
Untuk aplikasi yang memuat script dari sumber tertentu, sesuaikan CSP:
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.jsdelivr.net"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:"],
},
},
})
)CSP yang terlalu ketat bisa memblokir fungsi situs; yang terlalu longgar tidak berguna. Mulai ketat, lalu tambahkan sumber yang benar-benar dibutuhkan — bukan sebaliknya.
cors()app.use(cors()) (episode 5) mengizinkan semua origin — memudahkan development, berbahaya di produksi. Konfigurasi produksi membatasi ke daftar origin:
import cors from "cors"
import { config } from "./config.js"
app.use(
cors({
origin: config.corsOrigins,
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 86400,
})
)origin diambil dari config.corsOrigins (episode 16) — satu sumber kebenaran.credentials: true diperlukan jika memakai cookie (episode 12) — dan saat credentials aktif, origin tidak boleh *.maxAge menyimpan hasil preflight sehingga request CORS berikutnya lebih cepat.Important
CORS adalah kontrol browser, bukan keamanan server: ia mencegah browser lain membaca respons, tapi tidak menghentikan curl atau server-to-server. CORS menahan klien web tidak sah, bukan API abuse — rate limiting (episode 20) yang menangani yang terakhir.
SQL injection terjadi saat input disisipkan mentah ke string SQL. Parameterized query (episode 10) menutupnya — jangan pernah interpolasi:
// SALAH: rentan SQL injection
const { rows } = await pool.query(`SELECT * FROM users WHERE email = '${email}'`)
// BENAR: parameterized
const { rows } = await pool.query(
"SELECT * FROM users WHERE email = $1",
[email]
)MongoDB rentan versi JSON-nya: operator seperti $ne bisa dipakai untuk memanipulasi query jika input tidak dibersihkan:
// Payload berbahaya: { "email": { "$ne": null }, "password": { "$ne": null } }
const user = await User.findOne(req.body)
// Mengapa berbahaya: kondisi $ne bernilai true untuk hampir semua dokumenPertahanan paling efektif adalah validasi + sanitasi (episode 13) — schema zod menolak bentuk objek seperti { "$ne": null } sebelum sampai ke query. Validasi bukan pelengkap keamanan; ia adalah garis pertahanan utama.
XSS terjadi saat input pengguna dirender tanpa di-escape. Lapisan pertahanannya bertumpuk:
<%= %>, episode 11) — menetralkan script saat render.<script> dari input.export const commentSchema = z.object({
body: z
.string()
.trim()
.max(2000)
.transform((v) => v.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "")),
})CSRF menipu browser pengguna yang sudah login untuk mengirim request berbahaya — memakai cookie sesi yang otomatis ikut terkirim. Pertahanan utamanya:
SameSite cookie: lax atau strict (episode 12) memblokir pengiriman cookie lintas situs — menyelesaikan mayoritas kasus.SameSite=None, kirim token acak yang harus disertakan request. Untuk API berbasis JWT di header, CSRF praktis tidak relevan — header tidak otomatis terkirim lintas situs.app.use(
session({
secret: config.sessionSecret,
cookie: {
httpOnly: true,
sameSite: "lax",
secure: config.isProduction,
},
})
)Warning
Jangan mengikuti tutorial yang menonaktifkan sameSite atau httpOnly "biar lebih mudah". Dua atribut ini adalah dinding utama melawan XSS (httpOnly) dan CSRF (sameSite) — biarkan aktif di semua environment.
Rekap kontrol yang sudah dan belum kalian miliki:
Tanpa trust proxy, req.ip dan secure cookie berperilaku salah saat aplikasi di belakang reverse proxy/load balancer — IP yang tercatat adalah IP proxy, bukan client. Atur di episode 25.
Memakai Helmet dan server web yang juga memasang header (misal nginx) bisa menghasilkan nilai ganda. Tentukan satu pemilik header.
Hanya mengandalkan validasi frontend atau hanya sanitasi backend meninggalkan celah. Pertahanan bertumpuk (template escaping + CSP + httpOnly) adalah norma.
Tip
Uji hardening dengan alat otomatis: npx helmet-check (memeriksa header) dan OWASP ZAP (scan otomatis). Jadikan ini bagian dari workflow sebelum release — bukan reaksi setelah insiden.
Episode 18 menutup kategori serangan web utama: Helmet untuk header keamanan, CORS allowlist, SQLi/NoSQLi lewat parameterized query dan validasi, XSS lewat escaping + CSP + HttpOnly, serta CSRF lewat SameSite.
Inti yang harus dibawa pulang:
app.use(helmet()) memasang secure headers + CSP dalam satu baris.*.sameSite: "lax" menutup sebagian besar kasus; token untuk kasus ekstrem.Di episode 19 selanjutnya kita akan mengelola CVE & dependency management — contoh CVE nyata 2026, npm audit, dependabot, dan alur update dependency yang aman. Sampai jumpa di episode 19!