Skip to content

Commit 16e55e1

Browse files
committed
improved sanitization of HTML
1 parent 5ea381b commit 16e55e1

File tree

1 file changed

+49
-26
lines changed

1 file changed

+49
-26
lines changed

src/main.ts

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -565,37 +565,60 @@ class MappingInputProvider extends HTMLElement {
565565
* @param html Raw HTML provided by internal calls (never untrusted user input).
566566
* @returns A safe DocumentFragment ready to be inserted.
567567
*/
568+
const fragment = document.createDocumentFragment();
569+
if (!html.includes('<')) {
570+
// fast path plain text
571+
fragment.appendChild(document.createTextNode(html));
572+
return fragment;
573+
}
568574
const template = document.createElement('template');
569575
template.innerHTML = html;
570-
const allowedSpanClass = 'heroicons-outline--exclamation';
571-
const walker = (node: Node): Node | null => {
572-
if (node.nodeType === Node.ELEMENT_NODE) {
573-
const el = node as HTMLElement;
574-
if (el.tagName === 'BR') return el; // keep line breaks
575-
else if (el.tagName === 'EM' || el.tagName === 'STRONG' || el.tagName === 'B' || el.tagName === 'I') return el; // keep emphasis
576-
else if (
577-
el.tagName === 'SPAN' &&
578-
el.classList.length === 1 &&
579-
el.classList.contains(allowedSpanClass)
580-
) {
581-
// strip any other attributes for safety
582-
Array.from(el.attributes).forEach((attr) => {
583-
if (attr.name !== 'class') el.removeAttribute(attr.name);
584-
});
585-
return el;
576+
const allowedSimpleTags = new Set(['B', 'I']);
577+
578+
const sanitizeChildren = (srcParent: Node, destParent: Node) => {
579+
srcParent.childNodes.forEach((child) => walk(child, destParent));
580+
};
581+
582+
const walk = (node: Node, parent: Node) => {
583+
switch (node.nodeType) {
584+
case Node.TEXT_NODE: {
585+
if (node.textContent) parent.appendChild(document.createTextNode(node.textContent));
586+
return;
586587
}
587-
// Replace disallowed element with a text node of its textContent
588-
return document.createTextNode(el.textContent ?? '');
588+
case Node.ELEMENT_NODE: {
589+
const el = node as HTMLElement;
590+
const tag = el.tagName;
591+
// <br> allowed (no attributes kept)
592+
if (tag === 'BR') {
593+
parent.appendChild(document.createElement('br'));
594+
return;
595+
}
596+
// Emphasis tags: recreate element, drop all attributes, recurse into children
597+
if (allowedSimpleTags.has(tag)) {
598+
const neo = document.createElement(tag.toLowerCase());
599+
parent.appendChild(neo);
600+
sanitizeChildren(el, neo);
601+
return;
602+
}
603+
// Span: recreate and transfer entire class list (strip other attributes)
604+
if (tag === 'SPAN') {
605+
const span = document.createElement('span');
606+
span.className = el.className; // copy all classes
607+
parent.appendChild(span);
608+
sanitizeChildren(el, span);
609+
return;
610+
}
611+
// Disallowed wrapper: do not keep the element, but recursively sanitize its children
612+
sanitizeChildren(el, parent);
613+
return;
614+
}
615+
default: // ignore comments / others
616+
return;
589617
}
590-
if (node.nodeType === Node.TEXT_NODE) return node;
591-
return null; // drop comments or others
592618
};
593-
const outFrag = document.createDocumentFragment();
594-
Array.from(template.content.childNodes).forEach((child) => {
595-
const safe = walker(child);
596-
if (safe) outFrag.appendChild(safe);
597-
});
598-
return outFrag;
619+
620+
template.content.childNodes.forEach((n) => walk(n, fragment));
621+
return fragment;
599622
}
600623

601624
/**

0 commit comments

Comments
 (0)