Security Headers #

Dalam mengamankan aplikasi web, sering kali kita terlalu fokus mengunci bagian server (seperti database, backend, dan port jaringan) hingga melupakan titik serangan utama lainnya: sisi browser pengguna (client-side). Serangan seperti Cross-Site Scripting (XSS), Clickjacking, dan pembajakan MIME sniffing menargetkan celah keamanan pada interaksi browser dengan halaman web kita.

Salah satu cara termudah dan paling efisien untuk melindungi pengguna dari serangan tersebut adalah dengan menerapkan HTTP Security Headers di Nginx. Header ini adalah sekumpulan instruksi keamanan yang disisipkan oleh Nginx ke dalam setiap respons HTTP untuk mengaktifkan fitur perlindungan bawaan pada browser modern. Di artikel ini, kita akan membedah fungsionalitas setiap header keamanan utama, cara menyusun Content Security Policy (CSP) yang kokoh, menyembunyikan identitas software server, serta merapikan pengaturannya dalam bentuk berkas snippet yang siap digunakan ulang.

Bagaimana Security Headers Melindungi Browser? #

Browser modern dirancang dengan berbagai sistem keamanan internal yang canggih. Namun secara default, browser bersikap netral dan pasif demi menjaga kompatibilitas sejarah web. Dengan mengirimkan HTTP Security Headers, Nginx bertindak sebagai pemberi instruksi tegas yang memaksa browser memperketat perilakunya demi melindungi privasi dan integritas data pengguna.

Masing-masing header bertindak sebagai tameng untuk celah serangan yang spesifik. Mari kita bedah satu per satu:


X-Frame-Options: Menangkal Serangan Clickjacking #

Clickjacking adalah teknik penipuan di mana penyerang membungkus halaman web kita di dalam elemen iframe transparan di situs web mereka. Pengguna mengira mereka mengklik tombol hadiah di situs penyerang, padahal secara tidak sadar mereka sedang mengklik tombol transfer uang atau tombol hapus akun di situs kita yang tersembunyi di bawahnya.

Untuk mencegah situs kita dimasukkan ke dalam iframe di luar kehendak kita, kita menggunakan header X-Frame-Options:

# Opsi 1: Larang total situs kita dimasukkan ke dalam iframe oleh siapa pun
add_header X-Frame-Options "DENY" always;

# Opsi 2: Izinkan iframe hanya jika pemanggilnya berasal dari domain yang sama (Rekomendasi Umum)
add_header X-Frame-Options "SAMEORIGIN" always;
  • DENY: Pilihan terbaik jika aplikasi kita tidak pernah membutuhkan fitur pemuatan iframe di halaman lain.
  • SAMEORIGIN: Pilihan ideal untuk kebanyakan aplikasi web modern yang masih memerlukan pemuatan halaman internal sebagai dialog pop-up internal.
  • Pentingnya parameter always: Secara default, Nginx hanya menambahkan header pada respons dengan status code sukses (seperti 200 atau 301). Dengan menyertakan keyword always, Nginx menjamin header keamanan ini tetap dikirimkan bahkan pada respons error (seperti 404 atau 500), yang juga rentan diserang.

X-Content-Type-Options: Mencegah Eksploitasi MIME Sniffing #

MIME Sniffing adalah fitur browser untuk menebak jenis file berdasarkan isinya, mengabaikan header Content-Type resmi yang dikirimkan oleh server.

Celah ini sangat berbahaya jika situs kita mengizinkan pengguna mengunggah berkas teks atau gambar kustom. Sebagai contoh, seorang peretas dapat mengunggah berkas teks biasa yang berisi kode JavaScript berbahaya (misalnya kode pencuri cookie). Jika browser pengunjung mencoba menebak file tersebut dan mengeksekusinya sebagai JavaScript, serangan XSS telah berhasil terjadi.

Kita mengunci perilaku tebakan browser ini dengan direktif:

# Paksa browser mematuhi Content-Type resmi server
add_header X-Content-Type-Options "nosniff" always;

Dengan menyetel nosniff, browser dipaksa menuruti parameter header respons dari server. Jika Nginx mengirim berkas sebagai text/plain, browser akan menolak menjalankannya sebagai script.


Referrer-Policy: Mengontrol Kebocoran URL Asal #

Ketika pengguna mengklik tautan eksternal di halaman kita untuk pergi ke situs lain, browser secara default mengirimkan URL asal secara lengkap di dalam header HTTP Referer (misalnya Referer: https://example.com/akun/ubah-profil?token=rahasia). Hal ini dapat membocorkan URL internal sensitif atau token kueri ke server eksternal milik orang lain.

Kita membatasi volume kebocoran informasi ini menggunakan Referrer-Policy:

# Rekomendasi: Kirim full URL hanya untuk internal domain;
# Kirim hanya domain asal (tanpa path) untuk HTTPS luar;
# Jangan kirim apa pun jika pindah ke HTTP biasa yang tidak aman.
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Nilai strict-origin-when-cross-origin memberikan keseimbangan terbaik antara menjaga privasi data pengguna dan mempertahankan fungsi analisis pelacakan trafik sah (analytics tools).


X-XSS-Protection: Pengaman untuk Browser Lama #

Header ini dirancang untuk mengaktifkan sistem penyaring XSS bawaan pada browser lama (seperti Internet Explorer 8+ dan Google Chrome versi lama). Browser modern (seperti Chrome baru dan Firefox) kini telah menghapus dukungan header ini karena digantikan oleh Content Security Policy yang jauh lebih superior, namun menyisipkannya tetap berguna untuk backward compatibility:

# Aktifkan filter XSS, langsung blokir render halaman penuh jika serangan terdeteksi
add_header X-XSS-Protection "1; mode=block" always;

Permissions-Policy: Membatasi Akses Fitur Perangkat Keras Browser #

Permissions-Policy (sebelumnya bernama Feature-Policy) memungkinkan kita mengendalikan fitur API browser apa saja yang diizinkan berjalan di halaman kita atau di dalam iframe yang kita muat. Ini sangat penting untuk meminimalkan permukaan serangan jika situs kita diretas.

Sebagai contoh, jika aplikasi kita hanyalah sebuah blog teks sederhana, tidak ada alasan bagi browser untuk mengizinkan akses ke kamera, mikrofon, atau modul pembayaran kartu kredit:

# Nonaktifkan akses fitur sensitif untuk semua origin
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;

Content Security Policy (CSP): Dinding Pertahanan XSS Terkuat #

Content Security Policy (CSP) adalah header keamanan yang paling kuat sekaligus yang paling kompleks untuk dikonfigurasi. CSP bertindak sebagai penjaga gerbang yang memberitahu browser secara ketat dari mana saja aset (seperti skrip JavaScript, file CSS, gambar, font, dan koneksi API) boleh dimuat dan dijalankan.

Secara default, browser akan mengeksekusi kode JavaScript apa pun yang ditemukannya di dalam dokumen HTML, termasuk kode berbahaya yang disisipkan oleh peretas (serangan XSS). Dengan CSP aktif, browser akan memblokir setiap kode script yang asalnya tidak terdaftar di daftar aman kita.

Berikut adalah gambaran bagaimana CSP memfilter asal-usul skrip:

flowchart TD
    Request["Browser Menemukan Tag Script"] --> CheckCSP{"Apakah Sumber Script Ada di Daftar CSP?"}
    CheckCSP -->|Ya: dari domain sendiri atau 'self'| Exec["Eksekusi Script (Aman)"]
    CheckCSP -->|Ya: dari CDN eksternal terdaftar| Exec
    CheckCSP -->|Tidak: script inline atau domain tak dikenal| Block["Blokir Eksekusi & Kirim Laporan CSP (Cegah XSS!)"]
    
    classDef success fill:#10b981,stroke:#059669,color:#ffffff;
    classDef danger fill:#ef4444,stroke:#dc2626,color:#ffffff;
    class Exec success;
    class Block danger;

Pola Penulisan Kebijakan CSP yang Aman #

Merancang CSP membutuhkan ketelitian, karena jika ada satu domain aset eksternal yang lupa kita daftarkan, fitur tersebut akan langsung macet di browser pengguna.

Berikut adalah contoh kebijakan CSP dasar yang direkomendasikan:

add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://apis.google.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.stripe.com; frame-ancestors 'none';" always;
  • default-src 'self': Default fallback jika aturan lain tidak didefinisikan secara spesifik adalah hanya mengizinkan resource dari domain kita sendiri ('self').
  • script-src 'self' https://apis.google.com: JavaScript hanya boleh dimuat dari domain kita dan CDN resmi Google API.
  • style-src 'self' 'unsafe-inline': File CSS boleh dimuat dari domain kita. Penggunaan 'unsafe-inline' terkadang masih diperlukan jika kerangka kerja (framework) frontend kita menulis gaya CSS langsung di tag HTML.
  • frame-ancestors 'none': Mencegah situs kita dibungkus di dalam iframe oleh siapa pun (bertindak sebagai pengganti modern untuk X-Frame-Options).

[!TIP] Sebelum menerapkan CSP secara langsung ke pengguna produksi, gunakan header Content-Security-Policy-Report-Only terlebih dahulu. Browser tidak akan memblokir aset yang melanggar kebijakan, melainkan hanya akan mengirimkan laporan kesalahan ke endpoint URL yang kita tentukan untuk keperluan analisis dan perbaikan konfigurasi.


Menyembunyikan Identitas Server Nginx #

Praktik pengintaian (reconnaissance) peretas biasanya diawali dengan mencari tahu jenis web server dan versi spesifik yang kita gunakan (misalnya Server: nginx/1.18.0). Dengan mengetahui versi sistem, peretas dapat mencari database eksploitasi keamanan yang cocok untuk versi tersebut di internet.

Kita harus meminimalkan paparan informasi ini:

1. Sembunyikan Versi Nginx (server_tokens off;) #

Secara bawaan, Nginx menampilkan informasi versi pada header respons HTTP dan halaman kesalahan bawaan (seperti halaman error 404). Kita dapat mematikan penampilan versi ini dengan direktif server_tokens:

# Letakkan di dalam blok http
server_tokens off;

Setelah diaktifkan, header server kita hanya akan bertuliskan Server: nginx tanpa menampilkan versi angka di belakangnya.

2. Sembunyikan Nama Server Sepenuhnya (Menggunakan Headers More Module) #

Jika kita ingin bertindak lebih jauh dengan menghapus tulisan Server: nginx secara total atau menggantinya dengan nama lain kustom (misalnya Server: WebServer), kita membutuhkan modul pihak ketiga bernama headers-more:

# Instal modul headers-more di Ubuntu/Debian
sudo apt install libnginx-mod-http-headers-more-filter -y

Kemudian di konfigurasi Nginx:

# Hapus header Server sepenuhnya dari respons HTTP
more_clear_headers 'Server';

# Atau ganti nilainya secara dinamis
# more_set_headers 'Server: MyCustomSecureServer';

Mengorganisasi Security Headers dalam Snippet Modular #

Agar konfigurasi virtual host kita tetap bersih dan mudah dipelihara, kita sangat disarankan untuk mengelompokkan seluruh deklarasi header keamanan ke dalam satu berkas terpisah, misalnya /etc/nginx/snippets/security-headers.conf:

# /etc/nginx/snippets/security-headers.conf

# Proteksi Iframe Clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;

# Mencegah MIME Sniffing
add_header X-Content-Type-Options "nosniff" always;

# Kontrol Kebocoran Referrer
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Filter XSS Kompatibilitas
add_header X-XSS-Protection "1; mode=block" always;

# Pembatasan Fitur Perangkat Keras Browser
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;

# Hilangkan informasi versi server
server_tokens off;

Sekarang, di setiap berkas konfigurasi virtual host server block Nginx, kita cukup memanggil berkas snippet tersebut dengan satu baris direktif include:

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Masukkan seluruh Security Headers
    include snippets/security-headers.conf;

    root /var/www/html;
    
    location / {
        try_files $uri $uri/ =404;
    }
}

Setelah memuat ulang konfigurasi (sudo systemctl reload nginx), kita dapat memvalidasi apakah seluruh header telah terpasang dengan sukses menggunakan layanan analisis gratis seperti securityheaders.com.


Alternatif CSP Aman: Penggunaan CSP Nonce dan Hash #

Ketika menerapkan Content Security Policy (CSP) yang ketat, salah satu kendala terbesar kita adalah keberadaan Inline Script (kode JavaScript yang ditulis langsung di dalam tag <script> HTML) atau Inline Style (kode CSS di dalam tag <style> atau atribut style="...").

Secara default, CSP yang aman melarang eksekusi skrip inline demi mencegah hacker menyuntikkan kode berbahaya. Cara malas untuk mengakalinya adalah menambahkan aturan 'unsafe-inline' di CSP, tetapi hal ini meniadakan 90% fungsi perlindungan CSP dari serangan XSS.

Dua solusi standar industri yang aman untuk mengatasi masalah ini adalah:

1. CSP Nonce (Number Used Once) #

CSP Nonce bekerja dengan cara menghasilkan nilai string acak (cryptographic token) unik untuk setiap request halaman. Server menyisipkan nonce ini ke dalam header CSP dan setiap tag script HTML yang sah. Browser hanya akan mengeksekusi skrip inline yang memiliki nilai nonce yang cocok dengan header.

Di Nginx, jika kita menggunakan Nginx Plus atau modul generator kustom, kita bisa menghasilkan nonce dinamis. Namun untuk Nginx open-source biasa, nonce dinamis umumnya diatur langsung dari sisi backend aplikasi kita (misalnya Node.js/Express atau Laravel) dengan meneruskan header CSP ke Nginx, atau dihasilkan di Nginx menggunakan variabel $request_id:

# Contoh implementasi sederhana Nonce di Nginx menggunakan request ID
# (Catatan: Nilai $request_id diubah menjadi Base64 sebelum digunakan sebagai nonce)
# add_header Content-Security-Policy "script-src 'self' 'nonce-$request_id';" always;

Di dokumen HTML, backend kita harus menuliskan:

<script nonce="NILAI_REQUEST_ID_DARI_SERVER">
    console.log("Skrip inline ini sah dan akan dieksekusi oleh browser!");
</script>

2. CSP Hash (SHA-256/384/512) #

Jika skrip inline kita bersifat statis (nilainya tidak pernah berubah), kita tidak memerlukan token dinamis. Kita cukup menghitung hash SHA-256 dari seluruh teks di dalam tag <script> tersebut, lalu mencantumkan hash tersebut di dalam header CSP.

Contoh: Kita memiliki skrip inline berikut di HTML:

<script>alert('Halo Dunia');</script>

Hash SHA-256 dari teks alert('Halo Dunia'); (termasuk spasi secara presisi) adalah sha256-a7y.... Kita daftarkan hash ini di Nginx:

add_header Content-Security-Policy "script-src 'self' 'sha256-a7yGdhfF7hsg8DhsYshsDhDgs8sHdjs=';" always;

Browser akan membaca hash tersebut, menghitung hash skrip inline di halaman secara mandiri, dan hanya mengizinkan eksekusi jika hash-nya cocok.


CORS (Cross-Origin Resource Sharing) vs Security Headers #

Sering kali kita bingung membedakan antara Security Headers dan CORS Headers (Access-Control-Allow-Origin, dll.). Keduanya memiliki filosofi arah perlindungan yang bertolak belakang:

  • Security Headers: Membatasi apa saja yang boleh dilakukan oleh browser saat memuat halaman situs kita (melindungi situs kita dan pengguna dari eksploitasi).
  • CORS Headers: Memberikan pelonggaran aturan keamanan browser (Same-Origin Policy) agar domain luar yang berbeda diizinkan mengambil data/API dari server kita.

Jika kita membangun server API Nginx, kita harus mengonfigurasi CORS secara aman, hanya mengizinkan origin domain web resmi kita yang boleh mengakses API tersebut:

server {
    listen 443 ssl;
    server_name api.example.com;

    location / {
        # Izinkan hanya frontend resmi kita untuk mengambil data API
        add_header Access-Control-Allow-Origin "https://www.example.com" always;
        add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;

        # Tangani request OPTIONS preflight secara instan di Nginx tanpa membebani backend
        if ($request_method = 'OPTIONS') {
            add_header Access-Control-Allow-Origin "https://www.example.com" always;
            add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
            add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
            add_header Access-Control-Max-Age 1728000;
            add_header Content-Type 'text/plain; charset=utf-8';
            add_header Content-Length 0;
            return 204;
        }

        proxy_pass http://api_backend;
    }
}

Ringkasan #

  • Security Headers Wajib: Aktifkan minimal header X-Frame-Options, X-Content-Type-Options, dan Referrer-Policy pada setiap web server Nginx produksi untuk menutup celah serangan client-side umum.
  • Selalu Gunakan parameter always: Pastikan keyword always disertakan di setiap direktif add_header agar proteksi tetap dikirimkan saat respons server menghasilkan error.
  • Gunakan CSP Report Only: Terapkan kebijakan CSP secara bertahap menggunakan mode Report-Only terlebih dahulu untuk meminimalkan risiko website macet akibat aset eksternal yang tidak terdaftar.
  • Sembunyikan Versi Server: Gunakan server_tokens off; untuk mempersulit penyerang mengetahui versi sistem Nginx kita yang sedang aktif.
  • Konfigurasi Modular: Satukan seluruh direktif header keamanan ke dalam sebuah berkas snippet modular agar mudah di-include di seluruh virtual host tanpa duplikasi kode.

← Sebelumnya: Proteksi DoS   Berikutnya: Logging →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact