FlawOpen

flawopen.com/xss/Javascript

● CWE-79 · उच्च
सुरक्षा अनुसंधान · FlawOpen

JavaScript XSS: DOM, React और Express सुरक्षित करें

असुरक्षित DOM/React sinks और links JavaScript चला सकते हैं। Text APIs अपनाएँ, HTML साफ़ करें, URL जाँचें और Trusted Types लागू करें (CWE-79)।

💡 आसान भाषा में (ELI5)

एक थिएटर के cue board की कल्पना करें। यदि stage manager दर्शकों की हर पर्ची को मंच-निर्देश माने, तो पर्ची अनपेक्षित दृश्य जोड़ सकती है; सामान्य संवाद की तरह दिखाने पर वह केवल पाठ रहती है। Browser में innerHTML और React का dangerouslySetInnerHTML DOM से दिए गए पाठ को markup की तरह parse करवाते हैं, जबकि textContent उसे अक्षरशः दिखाता है। सामान्य पाठ escape करें, केवल जानबूझकर अनुमति दिए HTML को sanitize करें, और link बनाने से पहले URL schemes जाँचें।

इस पेज के मुख्य शब्द

innerHTML vs textContent
innerHTML, outerHTML या insertAdjacentHTML में मान देने से ब्राउज़र स्ट्रिंग को HTML की तरह पार्स करता है, इसलिए उसमें मौजूद <img onerror> एक सक्रिय एलिमेंट बन जाता है। textContent और createTextNode सिर्फ़ टेक्स्ट डालते हैं; मान का कोई हिस्सा कभी मार्कअप की तरह पार्स नहीं होता।
Template literals in res.send()
Express का res.send() स्ट्रिंग को डिफ़ॉल्ट रूप से text/html के रूप में भेजता है। template literal भेजने से पहले ${req.query.q} हल कर देता है, इसलिए रिस्पॉन्स में वही होता है जो यूज़र ने query string में डाला। req.query.q array या object भी हो सकता है, इसलिए टाइप जाँचें।
dangerouslySetInnerHTML + DOMPurify
React JSX में टेक्स्ट को अपने-आप एस्केप करता है; dangerouslySetInnerHTML इससे स्पष्ट छूट है। मान को DOMPurify.sanitize() से गुज़ारें, जो उसे ब्राउज़र के अपने पार्सर से पढ़ता है और React के डालने से पहले स्क्रिप्ट, इवेंट हैंडलर और ख़तरनाक URL हटा देता है।
javascript: URLs in href
JSX एट्रिब्यूट का मान एस्केप करता है, लेकिन href="javascript:..." एक वैध URL है जो क्लिक करने पर कोड चलाता है। new URL() से पार्स करें और सिर्फ़ http: व https: की अनुमति दें; URL पार्सर शुरुआती स्पेस और टैब हटा देता है जिन्हें साधारण prefix जाँच चूक जाती है।
Trusted Types
CSP डायरेक्टिव require-trusted-types-for 'script' के साथ समर्थित ब्राउज़र innerHTML जैसे sink में साधारण स्ट्रिंग ठुकरा देते हैं और सिर्फ़ आपकी बनाई policy (आम तौर पर DOMPurify बुलाने वाली) से बने TrustedHTML object स्वीकार करते हैं। नए sink चुपचाप नहीं जुड़ पाते।

फ्रेमवर्क मैपिंग

हमले का चरण-दर-चरण प्रवाह

चरण 1

इनपुट किसी sink तक पहुँचता है

सर्च क्वेरी, API से आया प्रोफ़ाइल फ़ील्ड या किसी दूसरे यूज़र का सेव किया लिंक स्ट्रिंग बनकर आता है और innerHTML, dangerouslySetInnerHTML, href या res.send() को दिया जाता है।

चरण 2

मान मार्कअप बन जाता है

हानिरहित टेस्ट मान <img src=x onerror=alert(1)> के साथ innerHTML और res.send() असली <img> एलिमेंट बनाते हैं; सेव किया javascript:alert(1) लिंक क्लिक करने लायक़ स्क्रिप्ट बन जाता है।

चरण 3

ब्राउज़र उसे चला देता है

इमेज लोड नहीं होती और onerror चलता है, या यूज़र लिंक पर क्लिक करता है। हमलावर के कुछ और किए बिना कोड तुरंत चलता है।

चरण 4

स्क्रिप्ट वेबसाइट बनकर काम करती है

यह पेज के origin के साथ चलती है: DOM पढ़ सकती है, पीड़ित की cookies या localStorage के टोकन से साइट के API बुला सकती है और यूज़र को दिखने वाली चीज़ें बदल सकती है।

सोर्स कोड: कमज़ोर बनाम सुरक्षित कार्यान्वयन

कमज़ोर कार्यान्वयन
// profile.jsx: नीचे का हर फ़ंक्शन किसी स्ट्रिंग को HTML की तरह पार्स होने देता है
import express from 'express';

const app = express();

// Express: भेजने से पहले query का मान HTML में डाल दिया जाता है
app.get('/search', (req, res) => {
  res.send(`<h1>Results for ${req.query.q}</h1>`);
});

// innerHTML name को मार्कअप की तरह पार्स करता है
export function showGreeting(el, name) {
  el.innerHTML = `Hello, ${name}`;
}

// React HTML को ठीक वैसा ही डाल देता है जैसा मिला
export function Bio({ html }) {
  return <div className="bio" dangerouslySetInnerHTML={{ __html: html }} />;
}

// लिंक पर क्लिक करते ही javascript: URL चलता है
export function Website({ url }) {
  return <a href={url}>Website</a>;
}
सुरक्षित और सुदृढ़ फ़िक्स
// profile.jsx: टेक्स्ट टेक्स्ट ही रहता है; HTML और URL पहले जाँचे जाते हैं
import DOMPurify from 'dompurify';
import escapeHtml from 'escape-html';
import express from 'express';

const app = express();

// सिर्फ़ स्ट्रिंग स्वीकार होती हैं, और उनका 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 कभी मार्कअप पार्स नहीं करता
export function showGreeting(el, name) {
  el.textContent = `Hello, ${name}`;
}

// डालने से पहले DOMPurify स्क्रिप्ट और इवेंट हैंडलर हटा देता है
export function Bio({ html }) {
  return <div className="bio" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;
}

// सिर्फ़ http और https URL बचते हैं; बाक़ी सब # बन जाता है
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>;
}

इंजीनियरिंग और सिस्टम सुरक्षा चेकलिस्ट

स्रोत