UNPKG

@exadel/esl

Version:

Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components

90 lines (89 loc) 3.81 kB
const ALWAYS_DISALLOWED_TAGS = ['script']; const DEFAULT_DISALLOWED_TAGS = ['template', 'iframe', 'object', 'embed', 'link', 'meta']; const DEFAULT_URL_ATTRIBUTES = ['src', 'srcset', 'data', 'href', 'xlink:href', 'action', 'formaction']; const DEFAULT_ALLOWED_URL_PROTOCOLS = ['', 'http:', 'https:', 'mailto:', 'tel:']; const normalizeList = (items) => { return new Set(items.map((item) => item.toLowerCase())); }; const createContext = ({ disallowedTags, allowedRoots, urlAttributes, allowedUrlProtocols }) => { return { disallowedTags: normalizeList([...ALWAYS_DISALLOWED_TAGS, ...(disallowedTags || DEFAULT_DISALLOWED_TAGS)]), allowedRoots: (allowedRoots === null || allowedRoots === void 0 ? void 0 : allowedRoots.length) ? normalizeList(allowedRoots) : null, urlAttributes: normalizeList(urlAttributes || DEFAULT_URL_ATTRIBUTES), allowedUrlProtocols: normalizeList(allowedUrlProtocols || DEFAULT_ALLOWED_URL_PROTOCOLS) }; }; const getTagName = (el) => el.localName.toLowerCase(); const STRIP_INVISIBLE = /[\u200B-\u200F\u202A-\u202E\u2066-\u2069]/g; // eslint-disable-next-line no-control-regex const STRIP_CONTROLS = /[\u0000-\u001F\u007F]/g; const getUrlProtocol = (value) => { var _a, _b; const normalizedForProtocolCheck = value .replace(/\s+/g, '') .replace(STRIP_CONTROLS, '') .replace(STRIP_INVISIBLE, ''); return (_b = (_a = /^[a-z][a-z\d+.-]*:/i.exec(normalizedForProtocolCheck)) === null || _a === void 0 ? void 0 : _a[0].toLowerCase()) !== null && _b !== void 0 ? _b : ''; }; const isUnsafeUrl = (value, context) => { const protocol = getUrlProtocol(value); return !context.allowedUrlProtocols.has(protocol); }; /** checks if the attribute is dangerous */ const isDangerousAttribute = (name, value, context) => { const attrName = name.toLowerCase(); if (context.urlAttributes.has(attrName) && isUnsafeUrl(value, context)) return true; return attrName.startsWith('on'); }; /** loops through each attribute, if it's dangerous, remove it */ const removeDangerousAttributes = (el, context) => { Array.from(el.attributes).forEach(({ name, value }) => { if (isDangerousAttribute(name, value, context)) { el.removeAttribute(name); } }); }; /** filters first-level nodes under the sanitized root container */ const filterRootNodes = (root, context) => { if (!context.allowedRoots) return; Array.from(root.childNodes).forEach((node) => { var _a; if (node.nodeType !== Node.ELEMENT_NODE) { node.remove(); return; } const el = node; if (!((_a = context.allowedRoots) === null || _a === void 0 ? void 0 : _a.has(getTagName(el)))) el.remove(); }); }; /** removes disallowed elements and malicious attributes from the element descendants */ const sanitizeDescendants = (root, context) => { Array.from(root.childNodes).forEach((node) => { if (node.nodeType !== Node.ELEMENT_NODE) return; const el = node; if (context.disallowedTags.has(getTagName(el))) { el.remove(); return; } removeDangerousAttributes(el, context); sanitizeDescendants(el, context); }); }; export function sanitize(html, options = {}) { if (typeof html === 'string') { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const body = doc.body || document.createElement('body'); return sanitize(body, options).innerHTML; } const context = createContext(options); // sanitizes html removeDangerousAttributes(html, context); filterRootNodes(html, context); sanitizeDescendants(html, context); return html; }