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
JavaScript
/* 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;