Security headers adalah header di response yang memberi tahu browser cara memperlakukan jawaban itu: jangan menebak jenis isinya, jangan tampilkan di dalam frame situs lain, selalu pakai HTTPS. API gateway adalah server yang berdiri di depan semua service API-mu, misalnya nginx, jadi semua response lewat situ dan header-nya cukup dipasang sekali. Bayangin label di paket: "Isi: dokumen. Jangan dipajang di etalase toko lain." Kurirnya (browser) menurut ke label itu, dan bagian pengiriman di depan gedung (gateway) menempelkan label yang sama ke setiap paket yang keluar, dari departemen mana pun.
🧪 Tebakan cepat: gateway nginx-mu memasang
add_header X-Content-Type-Options "nosniff";tanpa kataalways. Response200 OKmembawa header itu. Response404 Not Foundmembawanya juga atau gak? Jawabannya ada di bagian "Memasang di gateway nginx".
Header yang dipasang di setiap response API
| Header | Nilai | Gunanya |
|---|---|---|
X-Content-Type-Options |
nosniff |
browser wajib percaya Content-Type, gak boleh menebak jenis isinya |
X-Frame-Options |
DENY atau SAMEORIGIN |
response gak boleh tampil di dalam <iframe> situs lain |
Referrer-Policy |
strict-origin-when-cross-origin |
ke situs lain, browser cuma mengirim origin, bukan URL lengkap |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
browser selalu pakai HTTPS selama 1 tahun |
Cache-Control |
no-store |
response berisi data pribadi gak disimpan di cache |
Ada juga header yang harus dibuang: X-Powered-By: Express dan versi di Server: nginx/1.27.0. Header itu memberi tahu penyerang software dan versi apa yang kamu pakai, jadi dia tinggal mencari celah untuk versi itu.
nosniff: percaya label, jangan menebak
MIME sniffing adalah kebiasaan browser menebak jenis isi dari isinya sendiri, bukan dari header Content-Type. Tebakan ini bisa dimanfaatkan. Misalnya endpoint GET /api/v1/notes/7 mengembalikan catatan yang ditulis user dengan Content-Type: text/plain. Halaman di situs lain menulis:
<script src="https://api.myapp.com/api/v1/notes/7"></script>
Kalau isi catatan itu kebetulan JavaScript yang valid, browser tanpa nosniff mau menjalankannya sebagai script. Dengan X-Content-Type-Options: nosniff, browser memegang label text/plain dan menolak. Chrome menulis di console:
Refused to execute script from 'https://api.myapp.com/api/v1/notes/7' because its MIME type ('text/plain') is not executable, and strict MIME type checking is enabled.
Aturan yang sama berlaku untuk stylesheet: dengan nosniff, file yang Content-Type-nya bukan text/css gak dipakai sebagai CSS. Nilainya cuma satu: nosniff.
X-Frame-Options: DENY atau SAMEORIGIN
X-Frame-Options mengatur siapa yang boleh menampilkan response-mu di dalam <iframe>:
DENY: gak boleh di-frame oleh siapa pun, termasuk situsmu sendiri. Pilihan yang pas untuk API.SAMEORIGIN: cuma boleh di-frame oleh halaman dari origin yang sama (scheme, host, dan port sama).
Di pelajaran clickjacking kamu sudah kenal versi modernnya, CSP frame-ancestors 'none' (sama dengan DENY) dan frame-ancestors 'self' (sama dengan SAMEORIGIN). Memasang keduanya aman: browser modern memakai frame-ancestors, browser lama memakai X-Frame-Options.
Memasang di gateway nginx
Di gateway, header dipasang dengan add_header, dan info server disembunyikan dengan dua baris:
server {
listen 443 ssl;
server_name api.myapp.com;
server_tokens off;
proxy_hide_header X-Powered-By;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location /api/ {
proxy_pass http://backend:8000;
}
}
server_tokens off;mengubahServer: nginx/1.27.0jadiServer: nginx, tanpa versi.proxy_hide_header X-Powered-By;membuang headerX-Powered-Byyang dikirim backend sebelum response sampai ke browser.add_header ... always;menempelkan header ke response. Kataalwaysmembuatnya ikut juga di response error seperti 404 dan 500. Tanpaalways, nginx cuma menambahkannya ke kode 200, 201, 204, 206, 301, 302, 303, 304, 307, dan 308. Jadi response 404 tadi gak membawanosniff. Itu jawaban tebakannya.
Hasilnya, GET /api/v1/users/42 dan GET /api/v1/users/999 (404) sama-sama tiba dengan:
Server: nginx
Content-Type: application/json; charset=utf-8
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Strict-Transport-Security: max-age=31536000; includeSubDomains
Di level aplikasi: middleware Express
Kalau gak ada gateway, pasang header yang sama di aplikasi. Ini middleware Express yang jalan sebelum semua route:
app.disable("x-powered-by"); // buang X-Powered-By: Express
app.use((req, res, next) => {
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-Frame-Options", "DENY");
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
res.setHeader("Cache-Control", "no-store");
next();
});
app.disable("x-powered-by")mematikan headerX-Powered-Bybawaan Express.res.setHeader(name, value)menempelkan satu header ke response, dannext()melanjutkan ke route berikutnya.Cache-Control: no-storepaling pas dipasang di aplikasi, karena aplikasi yang tahu endpoint mana yang berisi data pribadi.
Package helmet memasang sebagian besar header ini sekaligus dengan app.use(helmet()).
💡 Gateway juga mengurus rate limit dan ukuran request
Gateway juga tempat yang pas untuk membatasi jumlah request (kamu sudah belajar alasannya di rate-limiting-security) dan ukuran body. Perhatikan letaknya: limit_req_zone wajib di blok http. Ditaruh di blok server, nginx menolak start dengan pesan "limit_req_zone" directive is not allowed here.
http {
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
server_name api.myapp.com;
location /api/ {
limit_req zone=api burst=20 nodelay;
client_max_body_size 10m;
proxy_pass http://backend:8000;
}
}
}
rate=10r/sartinya 10 request per detik per alamat IP, danburst=20mengizinkan lonjakan sampai 20 request tambahan sebelum nginx menjawab 503.client_max_body_size 10mmenolak body di atas 10 MB dengan 413.
⚠️ Jebakan yang sering ketemu
- Lupa
always. Header keamanan cuma ada di response 200, lalu hilang di 404 dan 500. Response error juga dibaca browser, jadi tambahkanalwaysdi setiapadd_header. add_headerdi dalamlocationmenghapus yang diserver. nginx cuma mewarisiadd_headerdari level atas kalau di level itu gak adaadd_headersama sekali. Satuadd_header Cache-Control "no-store" always;dilocation /api/bikin semua header keamanan dariserverhilang di situ. Ulangi header-nya dilocation, misalnya lewat fileinclude.- Nilai yang salah tulis.
nosnif, atauX-Frame-Options: ALLOW-FROM https://..., diabaikan browser. Nilainya harus persisnosniff,DENY, atauSAMEORIGIN. - Memasang
X-XSS-Protection: 1; mode=block. Filter XSS lama itu sudah dibuang dari browser modern. OWASP menyarankanX-XSS-Protection: 0atau gak dipasang sama sekali. Pertahanan XSS yang sebenarnya adalah escaping dan CSP.
TL;DR: Security headers adalah instruksi di response untuk browser, dan API gateway seperti nginx memasangnya sekali untuk semua service. Pasang X-Content-Type-Options: nosniff (browser gak boleh menebak jenis isi, jadi text/plain gak bisa jalan sebagai script), X-Frame-Options: DENY (atau SAMEORIGIN kalau origin sendiri boleh mem-frame), Referrer-Policy: strict-origin-when-cross-origin, dan Strict-Transport-Security. Buang X-Powered-By dan versi Server dengan proxy_hide_header dan server_tokens off. Di nginx, tulis always supaya header ikut di 404 dan 500, dan ingat add_header di location menghapus warisan dari server. Di Express, pasang header dengan res.setHeader di middleware dan matikan x-powered-by.