गैर-भरोसेमंद एचटीएमएल को सैनिटाइज़ करना

वेब सुविधाएं: Sanitizer API

उपयोगकर्ता के जनरेट किए गए एचटीएमएल को सुरक्षित तरीके से दिखाना, सुरक्षा से जुड़ी एक आम चुनौती है. नेटिव Sanitizer API, खतरनाक कॉन्टेंट (जैसे, <script> टैग या on* एट्रिब्यूट) को डीओएम तक पहुंचने से पहले हटाने के लिए, पहले से मौजूद और ब्राउज़र के लिए ऑप्टिमाइज़ किया गया तरीका उपलब्ध कराता है.

क्लाइंट-साइड सैनिटाइज़ेशन, कॉन्टेंट रेंडर करने वाले पेज को सुरक्षित रखता है. अगर यूज़र जनरेटेड कॉन्टेंट को कहीं और भी रेंडर किया जाता है, जैसे कि अन्य क्लाइंट, सर्वर से रेंडर किए गए पेज या ईमेल, तो उसे सर्वर पर भी सैनिटाइज़ करें. साथ ही, सुरक्षा के लिए कई लेयर का इस्तेमाल करें. इसके अलावा, यह भी पक्का करें कि सर्वर और क्लाइंट के सैनिटाइज़ेशन के नियमों में कोई अंतर न हो.

1. setHTML() की मदद से, एचटीएमएल को सुरक्षित तरीके से शामिल करना

Sanitizer API का इस्तेमाल करने का मुख्य तरीका, किसी भी Element पर setHTML() तरीके का इस्तेमाल करना है. यह तरीका, पार्स करने, सैनिटाइज़ करने, और इंसर्ट करने की प्रोसेस को एक ही चरण में पूरा करता है. इससे यह पक्का होता है कि कॉन्टेंट को खास तौर पर उस कॉन्टेक्स्ट के लिए सैनिटाइज़ किया जाए जिसमें उसे इंसर्ट किया जाएगा.

const untrustedHTML = `
  <p>Hello!</p>
  <script>alert('XSS')</script>
  <img src="x" onerror="alert('XSS')">
  <div onclick="doBadThing()">Click me</div>
`;

const container = document.getElementById('output');

// Sanitize and insert in one call
// The default sanitizer removes scripts and event handlers, plus other
// elements and attributes outside its built-in allowlist (such as style)
container.setHTML(untrustedHTML);

2. कस्टम सैनिटाइज़र कॉन्फ़िगरेशन का इस्तेमाल करना

Sanitizer इंस्टेंस बनाकर, सैनिटाइज़ेशन के नियमों को अपनी ज़रूरत के मुताबिक बनाया जा सकता है. इससे आपको यह तय करने में मदद मिलती है कि आपके इस्तेमाल के उदाहरण के लिए, कौनसे एलिमेंट और एट्रिब्यूट इस्तेमाल किए जा सकते हैं.

Element.setHTML() और Document.parseHTML() का इस्तेमाल करने पर, डिफ़ॉल्ट Sanitizer सेटिंग, एक्सएसएस के लिए असुरक्षित सभी एलिमेंट और एट्रिब्यूट हटा देती हैं. ऐसा Sanitizer नहीं बनाया जा सकता जो उन एलिमेंट को अनुमति देता हो. हालांकि, अनुमति वाले एलिमेंट पर और पाबंदियां लगाई जा सकती हैं.

// Define a restrictive configuration that only allows the specified elements and attributes.
const config = {
  elements: ["p", "b", "i", "strong", "em"],
  attributes: ["class"],
  replaceWithChildrenElements: ['div']
};

const mySanitizer = new Sanitizer(config);

// Apply custom sanitization
container.setHTML(untrustedHTML, { sanitizer: mySanitizer });

elements में मौजूद नहीं होने वाले एलिमेंट को उनके कॉन्टेंट के साथ हटा दिया जाता है. replaceWithChildrenElements में उन रैपर एलिमेंट की सूची बनाएं जिनका टेक्स्ट आपको रखना है (जैसे कि ऊपर दिया गया div). इससे एलिमेंट हट जाता है, लेकिन उसके चाइल्ड एलिमेंट बने रहते हैं.

अगर आपको ज़्यादा अनुमति देने वाले Sanitizer की ज़रूरत है, जो एक्सएसएस के लिए असुरक्षित कुछ एलिमेंट या एट्रिब्यूट की अनुमति देता है, तो Element.setHTMLUnsafe() और Document.parseHTMLUnsafe() के साथ कस्टम सैनिटाइज़र का इस्तेमाल किया जा सकता है.

3. Document.parseHTML() की मदद से, उपयोगकर्ता के इनपुट को पार्स करना

अगर आपको किसी ऐसे एचटीएमएल को पार्स करना है जिस पर भरोसा नहीं किया जा सकता और उसे लाइव DOM में तुरंत शामिल नहीं करना है, तो Document.parseHTML() का इस्तेमाल करें. यह तरीका, डिफ़ॉल्ट या कस्टम नियमों के मुताबिक सैनिटाइज़ किया गया Document दिखाता है. इससे, सैनिटाइज़ किए गए नोड को शामिल करने से पहले, उनकी जांच की जा सकती है या उनमें बदलाव किया जा सकता है. उदाहरण के लिए, झलक बनाने या टेक्स्ट निकालने के लिए.

नतीजे के तौर पर मिलने वाले नोड को स्ट्रिंग में सीरियलाइज़ करने और innerHTML की मदद से फिर से पार्स करने के बजाय, उन्हें पेज में ले जाएं. इससे म्यूटेशन एक्सएसएस (mXSS) को फिर से शामिल किया जा सकता है.

const rawHTML = '<p>Hello <script>console.log("bad")</script></p>';

const doc = Document.parseHTML(rawHTML);
// doc.body now contains the sanitized nodes: <p>Hello </p>
preview.replaceChildren(...doc.body.childNodes);

सैनिटाइज़र एपीआई के लिए फ़ॉलबैक और ब्राउज़र सहायता

अगर आपके टारगेट ब्राउज़र में नेटिव Sanitizer API उपलब्ध नहीं है, तो DOMPurify जैसी लाइब्रेरी सिर्फ़ तब लोड करें, जब नेटिव सपोर्ट मौजूद न हो. इससे, उन ब्राउज़र पर इसे भेजने से बचा जा सकता है जिन्हें इसकी ज़रूरत नहीं है. सुविधा का पता लगाने के लिए, in जांच का इस्तेमाल करें, ताकि जांच पुराने ब्राउज़र में भी काम करे.

let domPurifyPromise;

function loadDOMPurify() {
  // Code-split with your bundler so DOMPurify is only downloaded when needed
  domPurifyPromise ??= import('dompurify').then((mod) => mod.default ?? mod);
  return domPurifyPromise;
}

export async function safeSetHTML(el, html) {
  const elements = ['p', 'b', 'i', 'strong', 'em'];
  const attributes = ['class'];

  if ('setHTML' in Element.prototype) {
    // setHTML() accepts a plain configuration object as the sanitizer
    el.setHTML(html, { sanitizer: { elements, attributes } });
  } else {
    const DOMPurify = await loadDOMPurify();
    el.innerHTML = DOMPurify.sanitize(html, {
      ALLOWED_TAGS: [...elements, '#text'],
      ALLOWED_ATTR: attributes,
      // Match the native Sanitizer: strip aria-*/data-* and disallowed element content
      ALLOW_ARIA_ATTR: false,
      ALLOW_DATA_ATTR: false,
      KEEP_CONTENT: false
    });
  }
}

डिफ़ॉल्ट रूप से, DOMPurify aria-* और data-* एट्रिब्यूट (ALLOWED_ATTR तय होने पर भी) के साथ-साथ, हटाए जाने वाले एलिमेंट के टेक्स्ट कॉन्टेंट को भी बनाए रखता है. वहीं, नेटिव सैनिटाइज़र, aria-* और data-* एट्रिब्यूट को हटा देता है. साथ ही, अनुमति न दिए गए एलिमेंट को उनके कॉन्टेंट के साथ हटा देता है. ALLOW_ARIA_ATTR: false, ALLOW_DATA_ATTR: false, KEEP_CONTENT: false को सेट करने और #text को अनुमति देने से, DOMPurify नेटिव तरीके से काम करता है. DOMPurify में replaceWithChildrenElements के जैसा कोई विकल्प पहले से मौजूद नहीं है. इसलिए, ऐसे कॉन्फ़िगरेशन में इस विकल्प का इस्तेमाल न करें जिनमें मिलते-जुलते फ़ॉलबैक की ज़रूरत होती है.

अगर पेज पर भरोसेमंद टाइप (require-trusted-types-for 'script') लागू किए जाते हैं, तो innerHTML को स्ट्रिंग असाइन करने पर गड़बड़ी होती है. setHTML() को भरोसेमंद टाइप की नीति की ज़रूरत नहीं है, लेकिन फ़ॉलबैक को है: RETURN_TRUSTED_TYPE: true को DOMPurify.sanitize() में पास करें और अपने trusted-types सीएसपी डायरेक्टिव में इसकी dompurify नीति के नाम को अनुमति दें.