Memitigasi pembuatan skrip lintas situs (XSS) dengan Kebijakan Keamanan Konten (CSP) yang ketat

Lukas Weichselbaum
Lukas Weichselbaum

Dukungan Browser

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

Sumber

Pembuatan skrip lintas situs (XSS), kemampuan untuk memasukkan skrip berbahaya ke aplikasi web, telah menjadi salah satu kerentanan keamanan web terbesar selama lebih dari satu dekade.

Kebijakan Keamanan Konten (CSP) adalah lapisan keamanan tambahan yang membantu mengurangi XSS. Untuk mengonfigurasi CSP, tambahkan header HTTP Content-Security-Policy ke halaman web dan tetapkan nilai yang mengontrol resource yang dapat dimuat oleh agen pengguna untuk halaman tersebut.

Halaman ini menjelaskan cara menggunakan CSP berdasarkan nonce atau hash untuk mengurangi XSS, bukan CSP berbasis daftar yang diizinkan host yang biasa digunakan yang sering kali membuat halaman terkena XSS karena dapat diabaikan di sebagian besar konfigurasi.

Istilah penting: Nonce adalah angka acak yang hanya digunakan satu kali dan dapat Anda gunakan untuk menandai tag <script> sebagai tepercaya.

Istilah penting: Fungsi hash adalah fungsi matematika yang mengonversi nilai input menjadi nilai numerik yang dikompresi dan disebut hash. Anda dapat menggunakan hash (misalnya, SHA-256) untuk menandai tag <script> inline sebagai tepercaya.

Kebijakan Keamanan Konten berdasarkan nonce atau hash sering disebut sebagai CSP ketat. Jika aplikasi menggunakan CSP yang ketat, penyerang yang menemukan kelemahan injeksi HTML umumnya tidak dapat menggunakannya untuk memaksa browser menjalankan skrip berbahaya dalam dokumen yang rentan. Hal ini karena CSP ketat hanya mengizinkan skrip yang di-hash atau skrip dengan nilai nonce yang benar yang dihasilkan di server, sehingga penyerang tidak dapat menjalankan skrip tanpa mengetahui nonce yang benar untuk respons tertentu.

Mengapa Anda harus menggunakan CSP yang ketat?

Jika situs Anda sudah memiliki CSP yang terlihat seperti script-src www.googleapis.com, kemungkinan CSP tersebut tidak efektif terhadap serangan lintas situs. Jenis CSP ini disebut CSP daftar yang diizinkan. API ini memerlukan banyak penyesuaian dan dapat diabaikan oleh penyerang.

CSP ketat berdasarkan nonce atau hash kriptografis menghindari kendala ini.

Struktur CSP ketat

Kebijakan Keamanan Konten ketat dasar menggunakan salah satu header respons HTTP berikut:

CSP ketat berbasis nonce

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
Cara kerja CSP ketat berbasis nonce.

CSP ketat berbasis hash

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Properti berikut membuat CSP seperti ini "ketat" sehingga aman:

  • Fungsi ini menggunakan nonce 'nonce-{RANDOM}' atau hash 'sha256-{HASHED_INLINE_SCRIPT}' untuk menunjukkan tag <script> mana yang dipercaya developer situs untuk dijalankan di browser pengguna.
  • Fungsi ini menetapkan 'strict-dynamic' untuk mengurangi upaya deployment CSP berbasis nonce atau hash dengan secara otomatis mengizinkan eksekusi skrip yang dibuat oleh skrip tepercaya. Tindakan ini juga akan membatalkan pemblokiran penggunaan sebagian besar library dan widget JavaScript pihak ketiga.
  • Hal ini tidak didasarkan pada daftar yang diizinkan URL, sehingga tidak mengalami pengabaian CSP umum.
  • Fitur ini memblokir skrip inline yang tidak tepercaya seperti pengendali peristiwa inline atau URI javascript:.
  • Tindakan ini membatasi object-src untuk menonaktifkan plugin berbahaya seperti Flash.
  • Tindakan ini membatasi base-uri untuk memblokir injeksi tag <base>. Hal ini mencegah penyerang mengubah lokasi skrip yang dimuat dari URL relatif.

Mengadopsi CSP ketat

Untuk menerapkan CSP yang ketat, Anda harus:

  1. Tentukan apakah aplikasi Anda harus menetapkan CSP berbasis nonce atau hash.
  2. Salin CSP dari bagian Struktur CSP ketat dan tetapkan sebagai header respons di seluruh aplikasi Anda.
  3. Faktorkan ulang template HTML dan kode sisi klien untuk menghapus pola yang tidak kompatibel dengan CSP.
  4. Deploy CSP Anda.

Anda dapat menggunakan audit Praktik Terbaik Lighthouse (v7.3.0 dan yang lebih baru dengan tanda --preset=experimental) sepanjang proses ini untuk memeriksa apakah situs Anda memiliki CSP, dan apakah CSP tersebut cukup ketat untuk efektif melawan XSS.

Laporan Lighthouse memperingatkan bahwa tidak ada CSP yang ditemukan dalam mode penerapan.
Jika situs Anda tidak memiliki CSP, Lighthouse akan menampilkan peringatan ini.

Langkah 1: Putuskan apakah Anda memerlukan CSP berbasis nonce atau hash

Berikut cara kerja kedua jenis CSP ketat:

CSP berbasis nonce

Dengan CSP berbasis nonce, Anda akan membuat angka acak saat runtime, menyertakannya dalam CSP, dan mengaitkannya dengan setiap tag skrip di halaman. Penyerang tidak dapat menyertakan atau menjalankan skrip berbahaya di halaman Anda, karena mereka harus menebak angka acak yang benar untuk skrip tersebut. Hal ini hanya berfungsi jika angka tersebut tidak dapat ditebak, dan baru dibuat saat runtime untuk setiap respons.

Gunakan CSP berbasis nonce untuk halaman HTML yang dirender di server. Untuk halaman ini, Anda dapat membuat angka acak baru untuk setiap respons.

CSP berbasis hash

Untuk CSP berbasis hash, hash dari setiap tag skrip inline ditambahkan ke CSP. Setiap skrip memiliki hash yang berbeda. Penyerang tidak dapat menyertakan atau menjalankan skrip berbahaya di halaman Anda, karena hash skrip tersebut harus ada di CSP Anda agar dapat dijalankan.

Gunakan CSP berbasis hash untuk halaman HTML yang ditayangkan secara statis, atau halaman yang perlu di-cache. Misalnya, Anda dapat menggunakan CSP berbasis hash untuk aplikasi web satu halaman yang dibuat dengan framework seperti Angular, React, atau lainnya, yang disajikan secara statis tanpa rendering sisi server.

Langkah 2: Tetapkan CSP yang ketat dan siapkan skrip Anda

Saat menetapkan CSP, Anda memiliki beberapa opsi:

  • Mode hanya laporan (Content-Security-Policy-Report-Only) atau mode penerapan (Content-Security-Policy). Dalam mode hanya laporan, CSP belum akan memblokir resource, sehingga tidak ada yang rusak di situs Anda, tetapi Anda dapat melihat error dan mendapatkan laporan untuk apa pun yang akan diblokir. Secara lokal, saat Anda menetapkan CSP, hal ini tidak terlalu penting, karena kedua mode tersebut menampilkan error di konsol browser. Jika ada, mode penerapan dapat membantu Anda menemukan resource yang diblokir oleh draf CSP, karena memblokir resource dapat membuat halaman Anda terlihat rusak. Mode khusus laporan akan menjadi paling berguna nanti dalam prosesnya (lihat Langkah 5).
  • Tag <meta> header atau HTML. Untuk pengembangan lokal, tag <meta> dapat lebih mudah digunakan untuk menyesuaikan CSP dan dengan cepat melihat pengaruhnya terhadap situs Anda. Namun:
    • Nanti, saat men-deploy CSP dalam produksi, sebaiknya tetapkan sebagai header HTTP.
    • Jika ingin menetapkan CSP dalam mode khusus laporan, Anda harus menetapkannya sebagai header, karena tag meta CSP tidak mendukung mode khusus laporan.

Opsi A: CSP berbasis nonce

Tetapkan header respons HTTP Content-Security-Policy berikut di aplikasi Anda:

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Membuat nonce untuk CSP

Nonce adalah angka acak yang hanya digunakan satu kali per pemuatan halaman. CSP berbasis nonce hanya dapat memitigasi XSS jika penyerang tidak dapat menebak nilai nonce. Nonce CSP harus:

  • Nilai acak yang kuat secara kriptografis (idealnya panjangnya lebih dari 128 bit)
  • Baru dibuat untuk setiap respons
  • Dienkode dengan base64

Berikut beberapa contoh cara menambahkan nonce CSP di framework sisi server:

const app = express();

app.get('/', function(request, response) {
  // Generate a new random nonce value for every response.
  const nonce = crypto.randomBytes(16).toString("base64");

  // Set the strict nonce-based CSP response header
  const csp = `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`;
  response.set("Content-Security-Policy", csp);

  // Every <script> tag in your application should set the `nonce` attribute to this value.
  response.render(template, { nonce: nonce });
});

Menambahkan atribut nonce ke elemen <script>

Dengan CSP berbasis nonce, setiap elemen <script> harus memiliki atribut nonce yang cocok dengan nilai nonce acak yang ditentukan di header CSP. Semua skrip dapat memiliki nonce yang sama. Langkah pertama adalah menambahkan atribut ini ke semua skrip agar CSP mengizinkannya.

Opsi B: Header Respons CSP berbasis Hash

Tetapkan header respons HTTP Content-Security-Policy berikut di aplikasi Anda:

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Untuk beberapa skrip inline, sintaksisnya adalah sebagai berikut: 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'.

Memuat skrip yang bersumber secara dinamis

Anda dapat memuat skrip pihak ketiga secara dinamis menggunakan skrip inline.

Contoh cara menyisipkan skrip.
Diizinkan oleh CSP
<script>
  var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js'];

  scripts.forEach(function(scriptUrl) {
    var s = document.createElement('script');
    s.src = scriptUrl;
    s.async = false; // to preserve execution order
    document.head.appendChild(s);
  });
</script>
Agar skrip ini dapat berjalan, Anda harus menghitung hash skrip inline dan menambahkannya ke header respons CSP, yang menggantikan placeholder {HASHED_INLINE_SCRIPT}. Untuk mengurangi jumlah hash, Anda dapat menggabungkan semua skrip inline menjadi satu skrip. Untuk melihat cara kerjanya, lihat contoh ini dan kodenya.
Diblokir oleh CSP
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
CSP memblokir skrip ini karena tidak ditambahkan secara dinamis dan tidak memiliki atribut integrity yang cocok dengan sumber yang diizinkan.

Pertimbangan pemuatan skrip

Contoh skrip inline menambahkan s.async = false untuk memastikan foo dieksekusi sebelum bar, meskipun bar dimuat terlebih dahulu. Dalam cuplikan ini, s.async = false tidak memblokir parser saat skrip dimuat, karena skrip ditambahkan secara dinamis. Parser hanya berhenti saat skrip dieksekusi, seperti yang dilakukan untuk skrip async. Namun, dengan cuplikan ini, perlu diingat:

  • Satu atau kedua skrip mungkin dieksekusi sebelum dokumen selesai didownload. Jika Anda ingin dokumen siap pada saat skrip dijalankan, tunggu peristiwa DOMContentLoaded sebelum menambahkan skrip. Jika hal ini menyebabkan masalah performa karena skrip tidak mulai didownload lebih awal, gunakan tag pramuat lebih awal di halaman.
  • defer = true tidak melakukan apa pun. Jika Anda memerlukan perilaku tersebut, jalankan skrip secara manual saat diperlukan.

Langkah 3: Faktorkan ulang template HTML dan kode sisi klien

Pengendali peristiwa inline (seperti onclick="…", onerror="…") dan URI JavaScript (<a href="javascript:…">) dapat digunakan untuk menjalankan skrip. Artinya, penyerang yang menemukan bug XSS dapat memasukkan HTML semacam ini dan mengeksekusi JavaScript berbahaya. CSP berbasis nonce atau hash melarang penggunaan markup semacam ini. Jika situs Anda menggunakan salah satu pola ini, Anda harus memfaktorkan ulang menjadi alternatif yang lebih aman.

Jika mengaktifkan CSP di langkah sebelumnya, Anda akan dapat melihat pelanggaran CSP di konsol setiap kali CSP memblokir pola yang tidak kompatibel.

Laporan pelanggaran CSP di konsol developer Chrome.
Error konsol untuk kode yang diblokir.

Pada umumnya, perbaikannya mudah:

Memfaktorkan ulang pengendali peristiwa inline

Diizinkan oleh CSP
<span id="things">A thing.</span>
<script nonce="${nonce}">
  document.getElementById('things').addEventListener('click', doThings);
</script>
CSP mengizinkan pengendali peristiwa yang terdaftar menggunakan JavaScript.
Diblokir oleh CSP
<span onclick="doThings();">A thing.</span>
CSP memblokir pengendali peristiwa inline.

Memfaktorkan ulang URI javascript:

Diizinkan oleh CSP
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo').addEventListener('click', linkClicked);
</script>
CSP mengizinkan pengendali peristiwa yang terdaftar menggunakan JavaScript.
Diblokir oleh CSP
<a href="javascript:linkClicked()">foo</a>
CSP memblokir javascript: URI.

Menghapus eval() dari JavaScript

Jika aplikasi Anda menggunakan eval() untuk mengonversi serialisasi string JSON menjadi objek JS, Anda harus memfaktorkan ulang instance tersebut ke JSON.parse(), yang juga lebih cepat.

Jika tidak dapat menghapus semua penggunaan eval(), Anda masih dapat menetapkan CSP berbasis nonce yang ketat, tetapi Anda harus menggunakan kata kunci CSP 'unsafe-eval', yang membuat kebijakan Anda sedikit kurang aman.

Anda dapat menemukan contoh ini dan contoh pemfaktoran ulang lainnya di codelab CSP ketat ini:

Langkah 4 (Opsional): Tambahkan penggantian untuk mendukung versi browser lama

Dukungan Browser

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

Sumber

Jika Anda perlu mendukung versi browser yang lebih lama:

  • Penggunaan strict-dynamic memerlukan penambahan https: sebagai pengganti untuk versi Safari sebelumnya. Jika Anda melakukannya:
    • Semua browser yang mendukung strict-dynamic mengabaikan penggantian https:, sehingga hal ini tidak akan mengurangi kekuatan kebijakan.
    • Di browser lama, skrip yang bersumber dari eksternal hanya dapat dimuat jika berasal dari origin HTTPS. Hal ini kurang aman daripada CSP ketat, tetapi masih mencegah beberapa penyebab XSS umum seperti injeksi URI javascript:.
  • Untuk memastikan kompatibilitas dengan versi browser yang sangat lama (4 tahun ke atas), Anda dapat menambahkan unsafe-inline sebagai penggantian. Semua browser terbaru mengabaikan unsafe-inline jika nonce atau hash CSP ada.
Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';

Langkah 5: Men-deploy CSP

Setelah mengonfirmasi bahwa CSP Anda tidak memblokir skrip yang sah di lingkungan pengembangan lokal, Anda dapat men-deploy CSP ke staging, lalu ke lingkungan produksi:

  1. (Opsional) Deploy CSP Anda dalam mode khusus laporan menggunakan header Content-Security-Policy-Report-Only. Mode khusus laporan berguna untuk menguji perubahan yang berpotensi menyebabkan kerusakan seperti CSP baru dalam produksi sebelum Anda mulai menerapkan batasan CSP. Dalam mode khusus laporan, CSP Anda tidak memengaruhi perilaku aplikasi, tetapi browser masih menghasilkan error konsol dan laporan pelanggaran saat menemukan pola yang tidak kompatibel dengan CSP Anda, sehingga Anda dapat melihat apa yang akan rusak bagi pengguna akhir. Untuk mengetahui informasi selengkapnya, lihat Reporting API.
  2. Jika Anda yakin bahwa CSP tidak akan merusak situs untuk pengguna akhir, deploy CSP menggunakan header respons Content-Security-Policy. Sebaiknya tetapkan CSP Anda menggunakan sisi server header HTTP karena lebih aman daripada tag <meta>. Setelah Anda menyelesaikan langkah ini, CSP akan mulai melindungi aplikasi Anda dari XSS.

Batasan

CSP yang ketat umumnya memberikan lapisan keamanan tambahan yang kuat yang membantu memitigasi XSS. Pada umumnya, CSP mengurangi permukaan serangan secara signifikan, dengan menolak pola berbahaya seperti URI javascript:. Namun, berdasarkan jenis CSP yang Anda gunakan (nonce, hash, dengan atau tanpa 'strict-dynamic'), ada kasus saat CSP juga tidak melindungi aplikasi Anda:

  • Jika Anda membuat nonce untuk skrip, tetapi ada injeksi langsung ke isi atau parameter src dari elemen <script> tersebut.
  • Jika ada injeksi ke lokasi skrip yang dibuat secara dinamis (document.createElement('script')), termasuk ke dalam fungsi library apa pun yang membuat node DOM script berdasarkan nilai argumennya. Hal ini mencakup beberapa API umum seperti .html() jQuery, serta .get() dan .post() di jQuery < 3.0.
  • Jika ada injeksi template di aplikasi AngularJS lama. Penyerang yang dapat memasukkan kode ke template AngularJS dapat menggunakannya untuk menjalankan JavaScript arbitrer.
  • Jika kebijakan berisi 'unsafe-eval', injeksi ke eval(), setTimeout(), dan beberapa API lain yang jarang digunakan.

Developer dan engineer keamanan harus memperhatikan pola tersebut secara khusus selama peninjauan kode dan audit keamanan. Anda dapat menemukan detail selengkapnya tentang kasus ini di Kebijakan Keamanan Konten: Kesuksesan yang Bermasalah Antara Hardening dan Mitigasi.

Bacaan lebih lanjut