يُعدّ عرض محتوى HTML الذي ينشئه المستخدمون بأمان تحديًا شائعًا في مجال الأمان. توفّر واجهة Sanitizer API المضمّنة طريقة مدمجة ومحسّنة للمتصفّح لإزالة المحتوى الخطير (مثل علامات <script> أو سمات on*) قبل وصوله إلى نموذج DOM.
تحمي عملية التعقيم من جهة العميل الصفحة التي تعرض المحتوى. إذا تم عرض المحتوى من إنشاء المستخدمين نفسه في مكان آخر (عملاء آخرون أو صفحات يعرضها الخادم أو رسائل إلكترونية)، يجب تنقيته على الخادم أيضًا كإجراء دفاعي متعمّق، والتأكّد من عدم تعارض قواعد تنقية الخادم والعميل.
1. إدراج 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);
2. استخدام إعدادات أداة التعقيم المخصّصة
يمكنك تخصيص قواعد التعقيم من خلال إنشاء مثيل 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، يمكنك استخدام أداة تنقية مخصّصة مع Element.setHTMLUnsafe() وDocument.parseHTMLUnsafe().
3- تحليل إدخال المستخدم باستخدام Document.parseHTML()
إذا كنت بحاجة إلى تحليل رمز HTML غير موثوق به بدون إدراجه على الفور في نموذج 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
إذا لم تكن واجهة برمجة التطبيقات 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 في توجيه trusted-types CSP.