Vektor Serangan XSS pada Siklus Patching VDOM
Membangun Virtual DOM (VDOM) engine sendiri memberikan kontrol penuh atas performa render, namun membuka celah Cross-Site Scripting (XSS) serius jika proses patching atribut dan binding listener tidak divalidasi ketat. Pada fase rekonsiliasi (diffing), payload berbahaya umumnya masuk melalui tiga vektor utama: properti raw HTML seperti innerHTML, skema URI manipulatif seperti javascript: pada atribut navigasi (href, src), serta manipulasi fungsi callback event.
Masalah mendasar terjadi saat engine memperlakukan semua elemen dalam objek props secara identik menggunakan el.setAttribute(key, value) atau direct property assignment el[key] = value tanpa sanitasi skema dan validasi tipe.
Pemisahan Setter Properti vs Atribut DOM
Banyak custom VDOM engine gagal membedakan antara IDL (Interface Definition Language) attributes dan content attributes pada spesifikasi DOM. Menuliskan properti langsung ke objek DOM (el[key] = value) berbahaya jika nama atribut adalah innerHTML atau outerHTML.
Solusi arsitektural yang benar memerlukan isolasi setter:
- Gunakan
el.setAttribute()untuk atribut standar HTML. - Gunakan properti langsung hanya untuk form state reflection yang dinamis seperti
value,checked, dandisabled. - Blokir mutasi langsung ke properti internal browser seperti
innerHTML,outerHTML, dantextContentvia props generik. Jika raw HTML dibutuhkan, sediakan API terisolasi dengan penandaan eksplisit (misal:dangerouslySetInnerHTML).
Sanitasi URI Berbasis Safe-Protocol Allowlist
Atribut seperti href, src, action, dan formaction dapat mengeksekusi JavaScript jika nilainya diawali oleh skema pseudo-protokol javascript: atau data:text/html. Validasi berbasis blacklist mudah ditembus melalui encoding manipulatif (seperti whitespace atau karakter kontrol).
Terapkan validasi skema berbasis allowlist protokol aman (http:, https:, mailto:, tel:, atau relative URL):
const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|tel):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i;
const DATA_IMAGE_PATTERN = /^data:image\/(?:png|gif|jpeg|webp);base64,[a-z0-9+/=]+$/i;
function sanitizeUrl(val) {
if (typeof val !== 'string') return '';
const trimmed = val.trim();
if (SAFE_URL_PATTERN.test(trimmed) || DATA_IMAGE_PATTERN.test(trimmed)) {
return trimmed;
}
return 'about:blank';
}Manajemen Event Listener dan Mitigasi Bypass Memoization
Pada custom VDOM naif, diffing event listener sering dilakukan dengan menghapus listener lama dan mendaftarkan listener baru:
// Anti-pattern performa dan rawan memory leak
el.removeEventListener(eventType, oldHandler);
el.addEventListener(eventType, newHandler);Pendekatan ini lambat dan rentan bug referensi saat re-render cepat. Risiko keamanan muncul ketika attacker memalsukan handler objek yang memiliki method handleEvent custom namun mengeksekusi payload lain.
Pola paling aman dan efisien adalah Event Delegation menggunakan property listener wrapper sintetis atau synthetic dispatcher:
function patchEvent(el, name, nextValue) {
const eventName = name.slice(2).toLowerCase();
// Inisialisasi store listener terisolasi pada node DOM
const listeners = el._vei || (el._vei = {});
let invoker = listeners[eventName];
if (nextValue) {
// Validasi tipe: hanya function yang diizinkan sebagai handler
if (typeof nextValue !== 'function') {
console.warn(`Event handler for ${name} must be a function.`);
return;
}
if (!invoker) {
// Buat stable invoker wrapper
invoker = listeners[eventName] = (e) => {
// Eksekusi fungsi terkini tanpa binding ulang event listener
invoker.value(e);
};
invoker.value = nextValue;
el.addEventListener(eventName, invoker);
} else {
// Patch pointer tanpa menyentuh native DOM API
invoker.value = nextValue;
}
} else if (invoker) {
el.removeEventListener(eventName, invoker);
listeners[eventName] = undefined;
}
}Implementasi Patch Props Aman
Berikut adalah modul patch props terpadu yang memadukan sanitasi URI, pemisahan mutasi properti DOM, dan validasi event:
const DANGEROUS_PROPS = new Set(['innerHTML', 'outerHTML']);
const URI_ATTRS = new Set(['href', 'src', 'action', 'formaction']);
export function patchProp(el, key, prevValue, nextValue) {
// 1. Tangani Event Listener (on*)
if (key.startsWith('on')) {
patchEvent(el, key, nextValue);
return;
}
// 2. Blokir eksploitasi innerHTML langsung melalui props
if (DANGEROUS_PROPS.has(key)) {
console.error(`Refused to set dangerous property: ${key}`);
return;
}
// 3. Sanitasi Atribut Berbasis URL
if (URI_ATTRS.has(key)) {
nextValue = sanitizeUrl(nextValue);
}
// 4. Pisahkan Form Property Mutation vs Atribut HTML
if (key === 'value' || key === 'checked' || key === 'disabled') {
el[key] = nextValue ?? '';
return;
}
// 5. Update Atribut Standar
if (nextValue == null || nextValue === false) {
el.removeAttribute(key);
} else {
el.setAttribute(key, nextValue === true ? '' : nextValue);
}
}Trade-offs dan Pertimbangan Performa
Pemeriksaan regex pada atribut URI dan isolasi event invoker memberikan sedikit beban mikro-overhead pada proses patching. Namun, optimasi yang didapatkan dari wrapper invoker.value mengimbangi biaya tersebut karena mengurangi operasi manipulasi native event listener yang mahal (addEventListener/removeEventListener) secara drastis saat update node berlangsung.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!