FlawOpen

flawopen.com/xss/Javascript

● CWE-79 · Tinggi
Riset Keamanan · FlawOpen

XSS JavaScript: amankan DOM, React, dan Express

Gunakan API teks; sanitasi HTML, validasi URL, dan terapkan Trusted Types untuk DOM/React (CWE-79).

💡 Penjelasan Sederhana (ELI5)

Bayangkan panel aba-aba teater. Jika pengatur panggung menganggap setiap catatan penonton sebagai instruksi panggung, catatan itu dapat menambahkan adegan tak terduga; jika ditampilkan sebagai dialog biasa, isinya tetap teks. Di browser, innerHTML dan React dangerouslySetInnerHTML meminta DOM mengurai teks sebagai markup, sedangkan textContent menampilkannya secara harfiah. Escape teks biasa, sanitasi hanya sedikit HTML yang memang diizinkan, dan validasi skema URL sebelum membuat tautan.

Konsep Kunci & Istilah

innerHTML vs textContent
Menetapkan nilai ke innerHTML, outerHTML, atau insertAdjacentHTML membuat browser mengurai string sebagai HTML, sehingga <img onerror> di dalamnya menjadi elemen aktif. textContent dan createTextNode hanya menyisipkan teks; tidak ada bagian nilai yang diurai sebagai markup.
Template literals in res.send()
res.send() di Express mengirim string sebagai text/html secara bawaan. Template literal mengevaluasi ${req.query.q} sebelum dikirim, sehingga respons berisi apa pun yang dimasukkan pengguna ke query string. req.query.q juga bisa berupa array atau objek, jadi periksa tipenya.
dangerouslySetInnerHTML + DOMPurify
React meng-escape teks di JSX secara otomatis; dangerouslySetInnerHTML adalah pengecualian eksplisitnya. Lewatkan nilai melalui DOMPurify.sanitize(), yang menguraikannya dengan parser browser itu sendiri dan membuang skrip, event handler, serta URL berbahaya sebelum React menyisipkannya.
javascript: URLs in href
JSX meng-escape nilai atribut, tetapi href="javascript:..." adalah URL valid yang menjalankan kode saat diklik. Urai dengan new URL() dan hanya izinkan http: dan https:; parser URL membuang spasi dan tab di awal yang lolos dari pemeriksaan prefiks sederhana.
Trusted Types
Dengan direktif CSP require-trusted-types-for 'script', browser yang mendukung menolak string biasa di sink seperti innerHTML dan hanya menerima objek TrustedHTML yang dibuat oleh policy yang Anda definisikan, biasanya policy yang memanggil DOMPurify. Sink baru tidak bisa menyelinap tanpa ketahuan.

Pemetaan kerangka kerja

Alur Serangan Langkah demi Langkah

Langkah 1

Input sampai ke sink

Kueri pencarian, field profil yang dimuat dari API, atau tautan yang disimpan pengguna lain datang sebagai string dan diberikan ke innerHTML, dangerouslySetInnerHTML, href, atau res.send().

Langkah 2

Nilai berubah menjadi markup

Dengan nilai uji yang tidak berbahaya <img src=x onerror=alert(1)>, innerHTML dan res.send() menghasilkan elemen <img> sungguhan; tautan tersimpan javascript:alert(1) menjadi skrip yang bisa diklik.

Langkah 3

Browser menjalankannya

Gambar gagal dimuat dan memicu onerror, atau pengguna mengklik tautan. Kode langsung berjalan tanpa tindakan lanjutan dari penyerang.

Langkah 4

Skrip bertindak sebagai situs

Skrip berjalan dengan origin halaman: ia bisa membaca DOM, memanggil API situs dengan cookie korban atau token di localStorage, dan mengubah apa yang dilihat pengguna.

Kode Sumber: Rentan vs Aman

IMPLEMENTASI RENTAN
// profile.jsx: setiap fungsi di bawah membiarkan string diurai sebagai HTML
import express from 'express';

const app = express();

// Express: nilai query ditaruh di HTML sebelum dikirim
app.get('/search', (req, res) => {
  res.send(`<h1>Results for ${req.query.q}</h1>`);
});

// innerHTML mengurai name sebagai markup
export function showGreeting(el, name) {
  el.innerHTML = `Hello, ${name}`;
}

// React menyisipkan HTML persis seperti yang diterima
export function Bio({ html }) {
  return <div className="bio" dangerouslySetInnerHTML={{ __html: html }} />;
}

// URL javascript: berjalan saat tautan diklik
export function Website({ url }) {
  return <a href={url}>Website</a>;
}
PERBAIKAN AMAN & KUAT
// profile.jsx: teks tetap teks; HTML dan URL diperiksa lebih dulu
import DOMPurify from 'dompurify';
import escapeHtml from 'escape-html';
import express from 'express';

const app = express();

// hanya string yang diterima, dan di-escape sebagai HTML
app.get('/search', (req, res) => {
  const q = typeof req.query.q === 'string' ? req.query.q : '';
  res.send(`<h1>Results for ${escapeHtml(q)}</h1>`);
});

// textContent tidak pernah mengurai markup
export function showGreeting(el, name) {
  el.textContent = `Hello, ${name}`;
}

// DOMPurify membuang skrip dan event handler sebelum disisipkan
export function Bio({ html }) {
  return <div className="bio" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;
}

// hanya URL http dan https yang tersisa; sisanya menjadi #
function safeHref(url) {
  try {
    const parsed = new URL(url, window.location.origin);
    return ['http:', 'https:'].includes(parsed.protocol) ? parsed.href : '#';
  } catch {
    return '#';
  }
}

export function Website({ url }) {
  return <a href={safeHref(url)}>Website</a>;
}

Daftar Periksa Penguatan Sistem Rekayasa

Sumber