function stringToHTML(str: string): HTMLElement {
const parser = new DOMParser();
const doc = parser.parseFromString(str, 'text/html');
return doc.body;
}
function removeScripts(node: HTMLElement) {
node.querySelectorAll('script').forEach((script) => {
script.remove();
});
}
function isPossiblyDangerousAttribute(name: string, value: string): boolean {
if (['src', 'href', 'xlink:href'].includes(name)) {
// eslint-disable-next-line no-script-url -- defensively matching the scheme we want to strip
if (value.includes('javascript:') || value.includes('data:text/html')) return true;
}
return name.startsWith('on');
}
function removeAttributes(elem: Element) {
const attributes = elem.attributes;
for (let i = attributes.length - 1; i >= 0; i--) {
if (isPossiblyDangerousAttribute(attributes[i].name, attributes[i].value)) {
elem.removeAttributeNode(attributes[i]);
}
}
}
function clean(html: Element) {
const nodes = html.children;
for (let i = nodes.length - 1; i >= 0; i--) {
const node = nodes[i];
removeAttributes(node);
clean(node);
}
}
/** Strip scripts and event/handler attributes from an untrusted HTML string. */
export function cleanHTML(message: string): string {
const html = stringToHTML(message);
removeScripts(html);
clean(html);
return html.innerHTML;
}