پاک‌سازی HTML غیرقابل‌اعتماد

ویژگی‌های وب: Sanitizer API

نمایش ایمن HTML تولیدشده توسط کاربر یک چالش امنیتی رایج است. Sanitizer API بومی روشی توکار و بهینه‌سازی‌شده برای مرورگر ارائه می‌دهد تا محتوای خطرناک (مانند برچسب‌های <script> یا ویژگی‌های on*) را قبل‌از رسیدن به DOM حذف کند.

پاک‌سازی سمت کارخواه از صفحه‌ای که محتوا را ارائه می‌کند محافظت می‌کند. اگر همان محتوای تولیدشده توسط کاربر در جای دیگری نیز ارائه می‌شود (کارخواه‌های دیگر، صفحه‌های ارائه‌شده توسط سرور، یا ایمیل‌ها)، آن را در سرور و همچنین دفاع در عمق پاک‌سازی کنید و مطمئن شوید که قوانین پاک‌سازی سرور و کارخواه باهم تداخل نداشته باشند.

۱. درج ایمن HTML با setHTML()

روش اصلی استفاده از Sanitizer API ازطریق روش setHTML() در هر Element است. این روش تجزیه، پاک‌سازی، و درج را در یک مرحله انجام می‌دهد و تضمین می‌کند که محتوا به‌طور خاص برای بافتی که در آن درج خواهد شد پاک‌سازی می‌شود.

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);

۲. استفاده از پیکربندی‌های پاک‌کننده سفارشی

می‌توانید با ایجاد نمونه Sanitizer، قوانین پاک‌سازی را سفارشی‌سازی کنید. این کار به شما امکان می‌دهد دقیقاً مشخص کنید کدام عناصر و مشخصه‌ها برای مورد استفاده خاص شما مجاز هستند.

هنگام استفاده از Element.setHTML() و Document.parseHTML()، تنظیمات پیش‌فرض Sanitizer همه عناصر و مشخصه‌های ناامن ازنظر XSS را حذف می‌کند. نمی‌توانید 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 نیستند همراه با محتوایشان برداشته می‌شوند. عناصر پوشاننده را که می‌خواهید نوشتارشان حفظ شود (مثل div در بالا) در replaceWithChildrenElements فهرست کنید، که عنصر را برمی‌دارد اما فرزندانش را حفظ می‌کند.

اگر به Sanitizer آسان‌گیرتری نیاز دارید که برخی‌از عناصر یا مشخصه‌های ناامن ازنظر XSS را مجاز کند، می‌توانید از Sanitizer سفارشی با Element.setHTMLUnsafe() و Document.parseHTMLUnsafe() استفاده کنید.

۳. درحال تجزیه ورودی کاربر با Document.parseHTML()

اگر لازم است HTML غیرقابل‌اعتماد را بدون درج فوری آن در DOM زنده تجزیه کنید، از Document.parseHTML() استفاده کنید. این روش یک Document را برمی‌گرداند که طبق قوانین پیش‌فرض یا سفارشی پاک‌سازی شده است، بنابراین می‌توانید گره‌های پاک‌سازی‌شده را قبل‌از درج کردن بازرسی یا تبدیل کنید (برای مثال، برای ساختن پیش‌نمایش یا استخراج نوشتار).

گره‌های حاصل را به‌جای سریال‌سازی آن‌ها به رشته و تجزیه مجدد آن با innerHTML، به صفحه منتقل کنید، که می‌تواند XSS جهش (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

اگر 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 مشخص شده باشد) و همچنین محتوای نوشتاری عناصری را که حذف می‌کند نگه می‌دارد، درحالی‌که Sanitizer بومی مشخصه‌های aria-* و data-* را حذف می‌کند و عناصر غیرمجاز را همراه با محتوایشان برمی‌دارد. تنظیم ALLOW_ARIA_ATTR: false،‏ ALLOW_DATA_ATTR: false،‏ KEEP_CONTENT: false، و مجاز کردن #text باعث می‌شود DOMPurify با رفتار بومی مطابقت داشته باشد. DOMPurify گزینه داخلی معادل replaceWithChildrenElements ندارد، بنابراین از این گزینه در پیکربندی‌هایی که به جایگزین منطبق نیاز دارند اجتناب کنید.

اگر صفحه Trusted Types (require-trusted-types-for 'script') را اعمال کند، اختصاص دادن رشته به innerHTML باعث ایجاد خطا می‌شود. setHTML() به خط‌مشی «انواع مطمئن» نیاز ندارد، اما جایگزین به آن نیاز دارد: RETURN_TRUSTED_TYPE: true را به DOMPurify.sanitize() ارسال کنید و نام خط‌مشی dompurify آن را در دستور CSP trusted-types خود مجاز کنید.