@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
JavaScript
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;
}