Kullanıcı tarafından oluşturulan HTML'yi güvenli bir şekilde görüntülemek, yaygın bir güvenlik sorunudur. Yerel Sanitizer API, tehlikeli içerikleri (<script> etiketleri veya on* özellikleri gibi) DOM'a ulaşmadan önce kaldırmak için yerleşik ve tarayıcı için optimize edilmiş bir yöntem sunar.
İstemci tarafı temizleme, içeriği oluşturan sayfayı korur. Aynı kullanıcı tarafından oluşturulan içerik başka bir yerde de (diğer istemciler, sunucu tarafından oluşturulan sayfalar veya e-postalar) oluşturuluyorsa derinlemesine savunma için sunucuda da temizleyin ve sunucu ile istemci temizleme kurallarının çakışmadığından emin olun.
1. setHTML() ile HTML'yi güvenli bir şekilde ekleme
Sanitizer API'yi kullanmanın birincil yolu, herhangi bir Element üzerinde setHTML() yöntemini kullanmaktır. Bu yöntem; ayrıştırma, temizleme ve ekleme işlemlerini tek bir adımda gerçekleştirerek içeriğin, ekleneceği bağlama özel olarak temizlenmesini sağlar.
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. Özel Temizleyici Yapılandırmalarını Kullanma
Sanitizer örneği oluşturarak temizleme kurallarını özelleştirebilirsiniz. Bu sayede, özel kullanım alanınız için tam olarak hangi öğelere ve özelliklere izin verildiğini tanımlayabilirsiniz.
Element.setHTML() ve Document.parseHTML() kullanılırken varsayılan Sanitizer ayarları, XSS açısından güvenli olmayan tüm öğeleri ve özellikleri kaldırır. Bu öğelere izin veren daha esnek bir Sanitizer oluşturamazsınız ancak izin verilenleri daha da kısıtlayabilirsiniz.
// 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 içinde olmayan öğeler içerikleriyle birlikte kaldırılır. Metnini korumak istediğiniz sarmalayıcı öğeleri (yukarıdaki div gibi) replaceWithChildrenElements içinde listeleyin. Bu işlem, öğeyi kaldırır ancak alt öğelerini korur.
XSS açısından güvenli olmayan bazı öğelere veya özelliklere izin veren daha esnek bir Sanitizer gerekiyorsa Element.setHTMLUnsafe() ve Document.parseHTMLUnsafe() ile özel bir Sanitizer kullanabilirsiniz.
3. Document.parseHTML() ile Kullanıcı Girişini Ayrıştırma
Güvenilir olmayan HTML'yi canlı DOM'a hemen eklemeden ayrıştırmanız gerekiyorsa Document.parseHTML() kullanın. Bu yöntem, varsayılan veya özel kurallara göre temizlenmiş bir Document döndürür. Böylece, temizlenen düğümleri eklemeden önce inceleyebilir veya dönüştürebilirsiniz (ör. önizleme oluşturmak veya metin ayıklamak için).
Elde edilen düğümleri bir dizeye serileştirip innerHTML ile yeniden ayrıştırmak yerine sayfaya taşıyın. Bu işlem, mutasyon XSS'yi (mXSS) yeniden ortaya çıkarabilir.
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 için yedekler ve tarayıcı desteği
Yerel Sanitizer API hedef tarayıcılarınızda kullanılamıyorsa DOMPurify gibi bir kitaplığı yalnızca yerel destek eksik olduğunda yükleyin. Böylece, bu kitaplığı ihtiyaç duymayan tarayıcılara göndermemiş olursunuz. Özellik algılama için in kontrollerini kullanın. Böylece kontrolün kendisi eski tarayıcılarda çalışır.
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
});
}
}
Varsayılan olarak DOMPurify, aria-* ve data-* özelliklerini (ALLOWED_ATTR belirtildiğinde bile) ve kaldırdığı öğelerin metin içeriğini korur. Yerel Sanitizer ise aria-* ve data-* özelliklerini kaldırır ve izin verilmeyen öğeleri içerikleriyle birlikte kaldırır. ALLOW_ARIA_ATTR: false, ALLOW_DATA_ATTR: false, KEEP_CONTENT: false ayarlarını yapıp #text'ye izin verdiğinizde DOMPurify, yerel davranışla eşleşir. DOMPurify'da replaceWithChildrenElements ile eşdeğer yerleşik bir seçenek yoktur. Bu nedenle, eşleşen bir yedek gerektiren yapılandırmalarda bu seçeneği kullanmayın.
Sayfa, Güvenilir Türler'i (require-trusted-types-for 'script') zorunlu kılıyorsa innerHTML öğesine bir dize atandığında hata oluşur. setHTML() için Güvenilir Türler politikası gerekmez ancak yedek için gerekir: RETURN_TRUSTED_TYPE: true değerini DOMPurify.sanitize() yöntemine iletin ve trusted-types İGP yönergenizde dompurify politika adına izin verin.