UNPKG

amuchina

Version:

A work-in-progress HTML sanitizer that strives for: performance like window.Sanitizer, readiness like DOMPurify, and ability to run in a WebWorker like neither of those.

121 lines (120 loc) 7.45 kB
/* IMPORT */ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _Amuchina_configuration, _Amuchina_allowElements, _Amuchina_allowAttributes; import { NAMESPACES, NAMESPACES_ELEMENTS, NAMESPACES_ROOTS, NAMESPACES_PREFIXES } from './constants.js'; import { DEFAULTS } from './constants.js'; import { cloneDeep, isElementFunky, isElementAction, isElementIframe, isElementFormAction, isElementHyperlink, isScriptOrDataUrl, isScriptOrDataUrlLoose, traverseElements } from './utils.js'; /* MAIN */ //TODO: Add a decent test suite, possibly one from an existing trusted library class Amuchina { /* CONSTRUCTOR */ constructor(configuration = {}) { /* VARIABLES */ _Amuchina_configuration.set(this, void 0); _Amuchina_allowElements.set(this, void 0); _Amuchina_allowAttributes.set(this, void 0); /* API */ this.getConfiguration = () => { return cloneDeep(__classPrivateFieldGet(this, _Amuchina_configuration, "f")); }; this.sanitize = (input) => { //TODO: Support integration points (foreignObject and friends) //TODO: Support xlink:href, xml:id, xlink:title, xml:space, xmlns:xlink const allowElements = __classPrivateFieldGet(this, _Amuchina_allowElements, "f"); const allowAttributes = __classPrivateFieldGet(this, _Amuchina_allowAttributes, "f"); traverseElements(input, (node, parent) => { const namespace = node.namespaceURI || NAMESPACES.HTML; const namespaceParent = parent['namespaceURI'] || NAMESPACES.HTML; const elements = NAMESPACES_ELEMENTS[namespace]; const root = NAMESPACES_ROOTS[namespace]; const prefix = NAMESPACES_PREFIXES[namespace]; const tag = node.tagName.toLowerCase(); const tagPrefixed = `${prefix}${tag}`; const all = '*'; const allPrefixed = `${prefix}${all}`; if (!elements.has(tag) || !allowElements.has(tagPrefixed) || (namespace !== namespaceParent && tag !== root)) { parent.removeChild(node); } else { const attributes = node.getAttributeNames(); const attributesLength = attributes.length; if (attributesLength) { for (let i = 0; i < attributesLength; i++) { const attribute = attributes[i]; const allowedValues = allowAttributes[attribute]; if (!allowedValues || (!allowedValues.has(allPrefixed) && !allowedValues.has(tagPrefixed))) { node.removeAttribute(attribute); } } if (isElementFunky(node)) { if (isElementHyperlink(node)) { const href = node.getAttribute('href'); if (href && isScriptOrDataUrlLoose(href) && isScriptOrDataUrl(node.protocol)) { node.removeAttribute('href'); } } else if (isElementAction(node)) { if (isScriptOrDataUrl(node.action)) { node.removeAttribute('action'); } } else if (isElementFormAction(node)) { if (isScriptOrDataUrl(node.formAction)) { node.removeAttribute('formaction'); } } else if (isElementIframe(node)) { if (isScriptOrDataUrl(node.src)) { node.removeAttribute('formaction'); } node.setAttribute('sandbox', 'allow-scripts'); //TODO: This is kinda arbitrary, it should be customizable and more flexible } } } } }); return input; }; this.sanitizeFor = (element, input) => { throw new Error('"sanitizeFor" is not implemented yet'); }; const { allowComments, allowCustomElements, allowUnknownMarkup, blockElements, dropElements, dropAttributes } = configuration; if (allowComments === false) throw new Error('A false "allowComments" is not supported yet'); if (allowCustomElements) throw new Error('A true "allowCustomElements" is not supported yet'); if (allowUnknownMarkup) throw new Error('A true "allowUnknownMarkup" is not supported yet'); if (blockElements) throw new Error('"blockElements" is not supported yet, use "allowElements" instead'); if (dropElements) throw new Error('"dropElements" is not supported yet, use "allowElements" instead'); if (dropAttributes) throw new Error('"dropAttributes" is not supported yet, use "allowAttributes" instead'); __classPrivateFieldSet(this, _Amuchina_configuration, cloneDeep(DEFAULTS), "f"); const { allowElements, allowAttributes } = configuration; if (allowElements) __classPrivateFieldGet(this, _Amuchina_configuration, "f").allowElements = configuration.allowElements; if (allowAttributes) __classPrivateFieldGet(this, _Amuchina_configuration, "f").allowAttributes = configuration.allowAttributes; __classPrivateFieldSet(this, _Amuchina_allowElements, new Set(__classPrivateFieldGet(this, _Amuchina_configuration, "f").allowElements), "f"); __classPrivateFieldSet(this, _Amuchina_allowAttributes, Object.fromEntries(Object.entries(__classPrivateFieldGet(this, _Amuchina_configuration, "f").allowAttributes || {}).map(([element, attributes]) => [element, new Set(attributes)])), "f"); } } _Amuchina_configuration = new WeakMap(), _Amuchina_allowElements = new WeakMap(), _Amuchina_allowAttributes = new WeakMap(); /* STATIC API */ Amuchina.getDefaultConfiguration = () => { return cloneDeep(DEFAULTS); }; /* EXPORT */ export default Amuchina;