Aplikasi Node.js yang menerima masukan dari jaringan rentan terhadap sejumlah jenis serangan yang berulang di berbagai proyek. Topik ini membahas lima kerentanan umum, yaitu injection, XSS, CSRF, CORS, dan kelemahan header HTTP, beserta praktik yang mencegahnya. Setelah membaca topik ini, pola injection pada kueri basis data dapat dikenali, kebocoran data melalui XSS dapat dimitigasi dengan escaping dan Content Security Policy, serangan CSRF dapat ditangkal lewat token sinkron dan atribut cookie, kebijakan CORS dapat dikonfigurasi dengan benar, serta middleware helmet dapat dipasang pada Express untuk menambah header keamanan secara otomatis.
Injection#
Injection terjadi saat masukan klien ditempel langsung ke dalam perintah yang diterjemahkan oleh sistem lain, sehingga klien dapat mengubah struktur perintah itu. Bentuk yang paling lazim adalah SQL injection, yaitu saat masukan dipakai untuk menyusun string SQL lewat konkatenasi. Pola rentan berikut membentuk kueri dengan menempelkan req.body.email langsung ke string SQL.
// tidak-dijalankan
const email = req.body.email;
const sql = "SELECT id FROM pengguna WHERE email = '" + email + "'";
db.query(sql, (err, rows) => { /* ... */ });Bila klien mengirim email berisi ' OR '1'='1, string SQL yang dihasilkan menjadi SELECT id FROM pengguna WHERE email = '' OR '1'='1'. Karena '1'='1' selalu benar, kueri mengembalikan seluruh baris pada tabel pengguna. Variasi yang lebih berbahaya, misalnya '; DROP TABLE pengguna; --, dapat menambahkan perintah hapus tabel pada beberapa driver.
Pola aman memakai parameterized query, yaitu kueri yang ditulis dengan placeholder dan nilai yang dikirim secara terpisah. Driver basis data menangani pelolosan karakter sehingga masukan klien tidak dapat mengubah struktur kueri.
// tidak-dijalankan
const email = req.body.email;
const sql = "SELECT id FROM pengguna WHERE email = ?";
db.query(sql, [email], (err, rows) => { /* ... */ });Bentuk placeholder berbeda antar driver: tanda tanya ? pada mysql dan sqlite, $1 pada pg, serta nama yang dinamai pada better-sqlite3. Apapun bentuknya, prinsipnya sama: nilai tidak pernah disusun ke dalam string SQL oleh kode aplikasi, melainkan diteruskan sebagai argumen terpisah sehingga driver bertanggung jawab atas pelolosan.
XSS#
XSS (Cross-Site Scripting) terjadi saat masukan klien dimasukkan ke dalam respons HTML tanpa escaping, sehingga penyerang dapat menyisipkan elemen <script> yang dijalankan peramban pengguna lain. Serangan ini memungkinkan pencurian cookie, pengalihan permintaan, atau modifikasi halaman.
Pola rentan: membaca req.body.nama lalu menulisnya langsung ke respons HTML sebagai res.send("<h1>Halo " + nama + "</h1>"). Bila nama berisi <script>document.cookie</script>, string itu dikirim ke peramban sebagai elemen HTML aktif. Pola aman memakai template engine yang melakukan escaping otomatis, atau memanggil fungsi escape secara eksplisit.
// tidak-dijalankan
function escapeHtml(s) {
return s
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
app.get("/halaman", (req, res) => {
res.type("html");
res.send("<h1>Halo " + escapeHtml(req.query.nama || "") + "</h1>");
});Escaping menggantikan karakter yang bermakna dalam HTML (<, >, &, ", ') dengan entitas numerik sehingga peramban menampilkan karakter itu sebagai teks, bukan sebagai markah. Mesin templat seperti EJS, Pug, dan Handlebars melakukan escaping otomatis pada {{ nilai }} atau <%= nilai %>. Escaping manual hanya diperlukan bila string dikirim tanpa mesin templat, atau bila konten sengaja dimasukkan sebagai HTML mentah (kasus yang memerlukan sanitasi terpisah dengan pustaka seperti DOMPurify).
Lapisan pelindung tambahan adalah Content Security Policy (CSP), yaitu header yang membatasi sumber skrip, gaya, dan sumber daya lain yang boleh dimuat halaman. Kebijakan default-src 'self'; script-src 'self' menolak skrip yang tidak berasal dari situs yang sama, sehingga skrip yang disusupkan lewat XSS tidak dapat dieksekusi. CSP tidak menggantikan escaping, melainkan menjadi lapisan pertahanan kedua.
CSRF#
CSRF (Cross-Site Request Forgery) terjadi saat penyerang membuat pengguna yang sudah masuk ke sebuah situs mengirim permintaan ke situs itu tanpa disadari. Browser secara otomatis menyertakan cookie yang dimiliki untuk domain target pada setiap permintaan, termasuk permintaan yang dipicu dari situs lain. Akibatnya, permintaan yang berasal dari situs penyerang tetap membawa cookie session yang sah dan diterima server sebagai tindakan pengguna.
Alur serangan CSRF digambarkan pada diagram berikut. Pengguna sudah login ke site sah sehingga cookie session tersimpan di peramban. Pengguna kemudian mengunjungi site penyerang yang memuat form bertarget site sah. Form itu di-submit secara otomatis, dan peramban menyertakan cookie session karena cookie dikirim ke domain site sah tanpa mempedulikan asal permintaan.
Pertahanan utama CSRF adalah token sinkron, yaitu nilai acak yang diterbitkan server dan disematkan pada setiap form. Saat form di-submit, server membandingkan token pada form dengan token yang tersimpan pada session. Penyerang tidak dapat membaca token dari situs lain karena kebijakan same-origin membatasi akses silang domain.
// tidak-dijalankan
const crypto = require("node:crypto");
function tokenCSRF(req, res, next) {
if (!req.session.csrf) {
req.session.csrf = crypto.randomBytes(18).toString("base64url");
}
req.csrfToken = () => req.session.csrf;
next();
}
app.post("/transfer", tokenCSRF, (req, res) => {
if (req.body._csrf !== req.session.csrf) {
return res.status(403).json({ error: "CSRF_TOKEN_TIDAK_VALID" });
}
// proses transfer
});Atribut cookie modern SameSite=Strict atau SameSite=Lax menjadi lapisan tambahan. SameSite=Strict menolak pengiriman cookie pada permintaan yang berasal dari situs lain, sehingga serangan CSRF pada endpoint yang mengubah state tidak dapat membawa cookie. SameSite=Lax mengizinkan cookie pada navigasi GET tingkat atas (navigasi yang mengganti halaman utama peramban, bukan permintaan latar belakang), tetapi menolak pada POST lintas situs, yang cukup untuk sebagian besar aplikasi. Middleware csurf (atau penerusnya) dapat dipakai bila manajemen token manual dianggap berisiko.
CORS#
CORS (Cross-Origin Resource Sharing) adalah mekanisme peramban yang mengatur apakah JavaScript pada satu origin diperbolehkan membaca respons dari origin lain. Origin adalah gabungan skema, host, dan port, misalnya https://app.contoh.id dan https://api.contoh.id merupakan dua origin berbeda. Aturan default peramban melarang pembacaan respons lintas origin oleh JavaScript kecuali server mengirim header CORS yang mengizinkan.
Kesalahan paling lazim pada CORS adalah mengirim Access-Control-Allow-Origin: * pada endpoint yang memerlukan autentikasi. Kebijakan ini mengizinkan situs apa pun membaca respons, sehingga kredensial pengguna yang dikirim ke endpoint dapat diakses oleh situs penyerang. Untuk endpoint yang memerlukan cookie atau header Authorization, daftar origin yang diizinkan harus eksplisit.
// tidak-dijalankan
const cors = require("cors");
const daftarOrigin = ["https://app.contoh.id", "https://admin.contoh.id"];
app.use(cors({
origin: (origin, cb) => {
if (!origin || daftarOrigin.includes(origin)) return cb(null, true);
return cb(new Error("ORIGIN_TIDAK_DIIZINKAN"));
},
credentials: true,
}));Opsi credentials: true memerlukan Access-Control-Allow-Origin yang spesifik (bukan *) dan Access-Control-Allow-Credentials: true. Tanpa kombinasi ini, peramban menolak mengirim cookie lintas origin. Preflight (OPTIONS) dipicu untuk permintaan dengan metode selain GET/POST sederhana atau header kustom, sehingga middleware CORS perlu menanganinya.
Middleware helmet#
helmet adalah middleware Express yang menambah serangkaian header keamanan HTTP pada setiap respons. Header yang dipasang mencakup Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Content-Security-Policy, dan beberapa header lain. Setiap header menonaktifkan perilaku peramban yang lemah, seperti pengendalian tipe MIME, pembingkaian halaman oleh situs lain, dan pengiriman referrer penuh ke pihak ketiga.
Skrip berikut menjalankan server Express kecil dengan helmet() pada port acak, mengambil header respons melalui fetch, lalu menutup server. Header yang dicetak adalah hasil nyata dari middleware helmet versi 8 pada Express 5.
const express = require("express");
const helmet = require("helmet");
const app = express();
app.use(helmet());
app.get("/", (req, res) => res.json({ ok: true }));
const server = app.listen(0, async () => {
const port = server.address().port;
try {
const resp = await fetch(`http://127.0.0.1:${port}/`);
const headers = {};
for (const [k, v] of resp.headers.entries()) {
headers[k] = v;
}
console.log("status:", resp.status);
for (const k of Object.keys(headers).sort()) {
// header date bergantung pada waktu, dilewati agar keluaran dapat direproduksi
if (k === "date") continue;
console.log(`${k}: ${headers[k]}`);
}
} catch (e) {
console.log("error fetch:", e.message);
} finally {
server.close();
}
});status: 200
connection: keep-alive
content-length: 11
content-security-policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests
content-type: application/json; charset=utf-8
cross-origin-opener-policy: same-origin
cross-origin-resource-policy: same-origin
etag: W/"b-Ai2R8hgEarLmHKwesT1qcY913ys"
keep-alive: timeout=5
origin-agent-cluster: ?1
referrer-policy: no-referrer
strict-transport-security: max-age=31536000; includeSubDomains
x-content-type-options: nosniff
x-dns-prefetch-control: off
x-download-options: noopen
x-frame-options: SAMEORIGIN
x-permitted-cross-domain-policies: none
x-xss-protection: 0Beberapa header patut dicatat. Strict-Transport-Security: max-age=31536000; includeSubDomains menyatakan bahwa peramban wajib memakai HTTPS untuk domain dan subdomainnya selama satu tahun ke depan, sehingga downgrade ke HTTP ditolak. X-Content-Type-Options: nosniff melarang peramban menebak tipe konten yang berbeda dari Content-Type yang dideklarasikan server. X-Frame-Options: SAMEORIGIN melarang pembingkaian halaman oleh situs lain, sehingga serangan clickjacking terblokir. Content-Security-Policy membatasi sumber skrip dan sumber daya lain, yang menjadi lapisan pertahanan kedua terhadap XSS.
X-XSS-Protection: 0 sengaja menonaktifkan filter XSS lama peramban karena filter itu sendiri pernah memicu eksploitasi pada beberapa versi peramban. CSP dianggap sebagai pengganti yang lebih kuat dan terstandar. Header Origin-Agent-Cluster dan Cross-Origin-Opener-Policy mengatur isolasi proses peramban sehingga halaman kurang rentan terhadap serangan side-channel (serangan yang membocorkan data lewat pengamatan waktu atau perilaku perangkat keras) seperti Spectre, yaitu celah CPU yang terkenal dan memungkinkan pembacaan memori lintas proses.
Kesalahan umum#
Mengaktifkan CORS dengan origin: "*" dan credentials: true sekaligus. Peramban menolak kombinasi ini, sehingga permintaan dengan kredensial gagal. Server sebenarnya masih mengirim kedua header, tetapi peramban tidak membaca respons. Solusinya adalah memilih daftar origin eksplisit, bukan tanda bintang.
Mematikan middleware CSRF karena dianggap mengganggu pengujian. Endpoint yang mengubah state tetap rentan terhadap CSRF bahkan bila hanya dipakai internal. Pemutusan CSRF pada lingkungan pengujian menghilangkan perlindungan pada lingkungan produksi bila konfigurasinya tidak dipisah. Gunakan token CSRF sejak awal dan berikan pengecualian hanya pada endpoint yang memang tidak memerlukan autentikasi.
Mengandalkan helmet sebagai satu-satunya lapisan keamanan. helmet menambah header pelindung, tetapi tidak memvalidasi masukan, tidak memparameter kueri, dan tidak menambah token CSRF. Header Content-Security-Policy default membatasi skrip ke origin yang sama, tetapi tidak mencegah XSS yang berasal dari konten di origin yang sama. Helmet adalah lapisan pelengkap, bukan pengganti praktik validasi dan escaping.
Ringkasan#
- Injection dicegah dengan parameterized query, bukan konkatenasi string. Nilai diteruskan sebagai argumen terpisah, dan identifier dinamis divalidasi terhadap daftar putih.
- XSS dicegah dengan escaping otomatis oleh mesin templat dan lapisan kedua berupa Content Security Policy. Karakter yang bermakna dalam HTML diganti dengan entitas sebelum dicetak.
- CSRF ditangkal dengan token sinkron yang dibandingkan pada setiap permintaan yang mengubah state, serta atribut cookie
SameSite=LaxatauSameSite=Strictyang menolak pengiriman cookie lintas situs. - CORS mengatur pembacaan respons lintas origin oleh JavaScript. Endpoint yang memerlukan autentikasi harus memakai daftar origin eksplisit dengan
credentials: true, bukanorigin: "*". - helmet menambah header keamanan HTTP secara otomatis, termasuk
Strict-Transport-Security,X-Content-Type-Options,X-Frame-Options, danContent-Security-Policy. Header ini melindungi terhadap downgrade HTTP, MIME sniffing, clickjacking, dan sebagian XSS.
Referensi#
- helmet — npm — Dokumentasi middleware helmet, mencakup daftar header yang dipasang dan opsi konfigurasi tiap header.
- Content Security Policy Reference — MDN
— Rujukan direktif CSP beserta penjelasan
default-src,script-src, danframe-ancestors. - Cross-Site Request Forgery Prevention Cheat Sheet — OWASP
— Panduan OWASP tentang pencegahan CSRF, mencakup token sinkron dan atribut cookie
SameSite. - Cross-Origin Resource Sharing (CORS) — MDN — Penjelasan mekanisme CORS, header yang terlibat, dan interaksi dengan kredensial.