Engine diagram berbasis teks seperti Mermaid, PlantUML, atau parser custom yang mengompilasi DSL (Domain-Specific Language) menjadi SVG interaktif memiliki permukaan serangan luas. Masalah utama muncul ketika parser memetakan string input pengguna secara langsung ke dalam Document Object Model (DOM) SVG tanpa validasi ketat, membuka celah Stored Cross-Site Scripting (XSS).

Vektor Injeksi XSS pada Animasi SVG

SVG bukan sekadar format gambar statis; SVG adalah dokumen berbasis XML yang mendukung eksekusi skrip runtime melalui elemen SMIL (Synchronized Multimedia Integration Language) dan manipulasi atribut dinamis.

1. Eksekusi Script via Elemen Animasi SMIL

Penyerang dapat menyusupkan tag <animate>, <set>, atau <animateMotion> untuk memicu eksekusi kode JavaScript menggunakan event handler SMIL:

<!-- Vektor injeksi SMIL via atribut event handler -->
<svg xmlns="http://www.w3.org/2000/svg">
  <rect width="100" height="100" fill="blue">
    <animate 
      attributeName="x" 
      begin="0s" 
      dur="1s" 
      onbegin="alert(document.domain)" />
  </rect>
</svg>

2. Manipulasi Nilai Atribut Berbahaya via <set>

Elemen <set> dapat mengubah atribut target setelah jeda waktu tertentu. Skrip jahat dapat menambahkan URI javascript: secara dinamis ke atribut href atau xlink:href pada tag <a>, menghindari filter regex sederhana:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <a id="target_link">
    <text y="20">Klik di sini</text>
  </a>
  <set 
    xlink:href="#target_link" 
    attributeName="href" 
    to="javascript:alert(document.cookie)" 
    begin="0s" />
</svg>

Pipeline Sanitasi dan Hardening AST

Penyaringan berbasis string mentah menggunakan Regex rawan bypass karena variasi enkripsi entitas XML (misalnya &#x6a;&#x61;...). Pendekatan yang benar adalah melakukan sanitasi pada representasi Abstract Syntax Tree (AST) atau DOM hasil serialisasi.

Implementasi DOMPurify (Node.js/Browser)

Konfigurasi DOMPurify harus secara eksplisit membatasi tag SMIL dan hanya mengizinkan atribut animasi non-eksekutif:

import DOMPurify from 'dompurify';

export function sanitizeSvgOutput(rawSvgString: string): string {
  return DOMPurify.sanitize(rawSvgString, {
    USE_PROFILES: { svg: true, svgFilters: false },
    ADD_TAGS: ['animate', 'animateTransform'],
    ADD_ATTR: ['attributeName', 'dur', 'repeatCount', 'from', 'to', 'values', 'keyTimes'],
    FORBID_TAGS: ['script', 'foreignObject', 'set'],
    FORBID_ATTR: [
      'onbegin', 
      'onend', 
      'onrepeat', 
      'onload', 
      'onerror',
      'xlink:href',
      'href'
    ],
    ALLOW_DATA_ATTR: false
  });
}
Peringatan Keamanan: Jangan mengizinkan elemen foreignObject kecuali benar-benar diperlukan. Elemen ini memungkinkan penyematan konteks XHTML arbitrer di dalam SVG yang dapat mengeksekusi payload HTML standar.

Mitigasi ReDoS pada Grammar Parser Teks

Sebelum mencapai tahap rendering SVG, teks input diproses oleh lexer/parser DSL diagram. Pola tokenisasi yang ceroboh rentan terhadap serangan Regular Expression Denial of Service (ReDoS) akibat catastrophic backtracking.

Vulnerabilitas Catastrophic Backtracking

Pola regex parsing node label yang memiliki pengelompokan tumpang-tindih (overlapping groups):

// VULNERABLE: Menangani label multi-kata dengan spasi opsional
const NODE_REGEX = /^\[([a-zA-Z0-9]+(\s*))+\]$/;

// Input berbahaya:
// const payload = '[' + 'a'.repeat(35) + '!'; // Menyebabkan CPU hanging

Solusi: Atomic Grouping / Linear Deterministic Parsing

Gunakan ekspresi deterministik tanpa nesting quantifier atau gunakan library parser generator berbasis CFG (Context-Free Grammar) seperti Chevrotain atau Lezer yang berjalan dalam waktu linear $O(n)$:

// SAFE: Menghindari nested quantifiers dan overlapping branches
const SAFE_NODE_REGEX = /^\[([a-zA-Z0-9]+(?:\s+[a-zA-Z0-9]+)*)\]$/;

Isolasi Eksekusi via CSP dan Iframe Sandboxing

Sanitasi aplikasi lapis pertama (DOMPurify) harus digabungkan dengan isolasi runtime menggunakan Defense-in-Depth. Render output SVG di dalam <iframe> terisolasi dengan atribut sandbox tanpa flag allow-scripts atau allow-same-origin.

<iframe 
  title="Diagram Output" 
  sandbox="allow-downloads"
  srcdoc="<svg xmlns='http://www.w3.org/2000/svg'>...</svg>"
  csp="default-src 'none'; style-src 'unsafe-inline'; sandbox;"
  width="800" 
  height="600">
</iframe>

Dengan konfigurasi tersebut, jika payload injeksi lolos dari sanitizer AST, browser tetap memblokir eksekusi skrip karena konteks dokumen iframe memiliki hak akses origin null dan menolak eksekusi inline script.

Automated Security Test Suite

Terapkan unit test untuk memverifikasi bahwa parser secara konsisten menolak atau membersihkan vektor serangan animasi:

import { describe, it, expect } from 'vitest';
import { sanitizeSvgOutput } from './svgSanitizer';

describe('SVG Parser Security Tests', () => {
  it('harus menghapus event handler SMIL onbegin', () => {
    const attackPayload = `<svg><rect><animate attributeName="width" dur="5s" onbegin="alert(1)"/></rect></svg>`;
    const clean = sanitizeSvgOutput(attackPayload);
    expect(clean).not.toContain('onbegin');
    expect(clean).not.toContain('alert(1)');
  });

  it('harus memblokir elemen set yang memodifikasi atribut target', () => {
    const attackPayload = `<svg><set attributeName="href" to="javascript:alert(1)"/></svg>`;
    const clean = sanitizeSvgOutput(attackPayload);
    expect(clean).not.toContain('<set');
  });

  it('harus mempertahankan atribut animasi yang aman', () => {
    const validPayload = `<svg><circle r="10"><animate attributeName="r" from="10" to="20" dur="2s"/></circle></svg>`;
    const clean = sanitizeSvgOutput(validPayload);
    expect(clean).toContain('attributeName="r"');
    expect(clean).toContain('dur="2s"');
  });
});