;(function(factory) { 'use strict'; /* global window: false, define: false, module: false */ var root = typeof window === 'undefined' ? null : window; if (typeof define === 'function' && define.amd) { define(function(){ return factory(root); }); } else if (typeof module !== 'undefined') { module.exports = factory(root); } else { root.DOMPurify = factory(root); } }(function factory(window) { 'use strict'; var DOMPurify = function(window) { return factory(window); }; /** * Version label, exposed for easier checks * if DOMPurify is up to date or not */ DOMPurify.version = '0.7.4'; if (!window || !window.document || window.document.nodeType !== 9) { // not running in a browser, provide a factory function // so that you can pass your own Window DOMPurify.isSupported = false; return DOMPurify; } var document = window.document; var originalDocument = document; var DocumentFragment = window.DocumentFragment; var HTMLTemplateElement = window.HTMLTemplateElement; var NodeFilter = window.NodeFilter; var NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap; var Text = window.Text; var Comment = window.Comment; var DOMParser = window.DOMParser; // As per issue #47, the web-components registry is inherited by a // new document created via createHTMLDocument. As per the spec // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries) // a new empty registry is used when creating a template contents owner // document, so we use that as our parent document to ensure nothing // is inherited. if (typeof HTMLTemplateElement === 'function') { var template = document.createElement('template'); if (template.content && template.content.ownerDocument) { document = template.content.ownerDocument; } } var implementation = document.implementation; var createNodeIterator = document.createNodeIterator; var getElementsByTagName = document.getElementsByTagName; var createDocumentFragment = document.createDocumentFragment; var importNode = originalDocument.importNode; var hooks = {}; /** * Expose whether this browser supports running the full DOMPurify. */ DOMPurify.isSupported = typeof implementation.createHTMLDocument !== 'undefined' && document.documentMode !== 9; /* Add properties to a lookup table */ var _addToSet = function(set, array) { var l = array.length; while (l--) { if (typeof array[l] === 'string') { array[l] = array[l].toLowerCase(); } set[array[l]] = true; } return set; }; /* Shallow clone an object */ var _cloneObj = function(object) { var newObject = {}; var property; for (property in object) { if (object.hasOwnProperty(property)) { newObject[property] = object[property]; } } return newObject; }; /** * We consider the elements and attributes below to be safe. Ideally * don't add any new ones but feel free to remove unwanted ones. */ /* allowed element names */ var ALLOWED_TAGS = null; var DEFAULT_ALLOWED_TAGS = _addToSet({}, [ // HTML 'a','abbr','acronym','address','area','article','aside','audio','b', 'bdi','bdo','big','blink','blockquote','body','br','button','canvas', 'caption','center','cite','code','col','colgroup','content','data', 'datalist','dd','decorator','del','details','dfn','dir','div','dl','dt', 'element','em','fieldset','figcaption','figure','font','footer','form', 'h1','h2','h3','h4','h5','h6','head','header','hgroup','hr','html','i', 'img','input','ins','kbd','label','legend','li','main','map','mark', 'marquee','menu','menuitem','meter','nav','nobr','ol','optgroup', 'option','output','p','pre','progress','q','rp','rt','ruby','s','samp', 'section','select','shadow','small','source','spacer','span','strike', 'strong','style','sub','summary','sup','table','tbody','td','template', 'textarea','tfoot','th','thead','time','tr','track','tt','u','ul','var', 'video','wbr', // SVG 'svg','altglyph','altglyphdef','altglyphitem','animatecolor', 'animatemotion','animatetransform','circle','clippath','defs','desc', 'ellipse','filter','font','g','glyph','glyphref','hkern','image','line', 'lineargradient','marker','mask','metadata','mpath','path','pattern', 'polygon','polyline','radialgradient','rect','stop','switch','symbol', 'text','textpath','title','tref','tspan','view','vkern', // SVG Filters 'feBlend','feColorMatrix','feComponentTransfer','feComposite', 'feConvolveMatrix','feDiffuseLighting','feDisplacementMap', 'feFlood','feFuncA','feFuncB','feFuncG','feFuncR','feGaussianBlur', 'feMerge','feMergeNode','feMorphology','feOffset', 'feSpecularLighting','feTile','feTurbulence', //MathML 'math','menclose','merror','mfenced','mfrac','mglyph','mi','mlabeledtr', 'mmuliscripts','mn','mo','mover','mpadded','mphantom','mroot','mrow', 'ms','mpspace','msqrt','mystyle','msub','msup','msubsup','mtable','mtd', 'mtext','mtr','munder','munderover', //Text '#text' ]); /* Allowed attribute names */ var ALLOWED_ATTR = null; var DEFAULT_ALLOWED_ATTR = _addToSet({}, [ // HTML 'accept','action','align','alt','autocomplete','background','bgcolor', 'border','cellpadding','cellspacing','checked','cite','class','clear','color', 'cols','colspan','coords','datetime','default','dir','disabled', 'download','enctype','face','for','headers','height','hidden','high','href', 'hreflang','id','ismap','label','lang','list','loop', 'low','max', 'maxlength','media','method','min','multiple','name','noshade','novalidate', 'nowrap','open','optimum','pattern','placeholder','poster','preload','pubdate', 'radiogroup','readonly','rel','required','rev','reversed','rows', 'rowspan','spellcheck','scope','selected','shape','size','span', 'srclang','start','src','step','style','summary','tabindex','title', 'type','usemap','valign','value','width','xmlns', // SVG 'accent-height','accumulate','additivive','alignment-baseline', 'ascent','attributename','attributetype','azimuth','basefrequency', 'baseline-shift','begin','bias','by','clip','clip-path','clip-rule', 'color','color-interpolation','color-interpolation-filters','color-profile', 'color-rendering','cx','cy','d','dx','dy','diffuseconstant','direction', 'display','divisor','dur','edgemode','elevation','end','fill','fill-opacity', 'fill-rule','filter','flood-color','flood-opacity','font-family','font-size', 'font-size-adjust','font-stretch','font-style','font-variant','font-weight', 'fx', 'fy','g1','g2','glyph-name','glyphref','gradientunits','gradienttransform', 'image-rendering','in','in2','k','k1','k2','k3','k4','kerning','keypoints', 'keysplines','keytimes','lengthadjust','letter-spacing','kernelmatrix', 'kernelunitlength','lighting-color','local','marker-end','marker-mid', 'marker-start','markerheight','markerunits','markerwidth','maskcontentunits', 'maskunits','max','mask','mode','min','numoctaves','offset','operator', 'opacity','order','orient','orientation','origin','overflow','paint-order', 'path','pathlength','patterncontentunits','patterntransform','patternunits', 'points','preservealpha','r','rx','ry','radius','refx','refy','repeatcount', 'repeatdur','restart','result','rotate','scale','seed','shape-rendering', 'specularconstant','specularexponent','spreadmethod','stddeviation','stitchtiles', 'stop-color','stop-opacity','stroke-dasharray','stroke-dashoffset','stroke-linecap', 'stroke-linejoin','stroke-miterlimit','stroke-opacity','stroke','stroke-width', 'surfacescale','targetx','targety','transform','text-anchor','text-decoration', 'text-rendering','textlength','u1','u2','unicode','values','viewbox', 'visibility','vert-adv-y','vert-origin-x','vert-origin-y','word-spacing', 'wrap','writing-mode','xchannelselector','ychannelselector','x','x1','x2', 'y','y1','y2','z','zoomandpan', // MathML 'accent','accentunder','bevelled','close','columnsalign','columnlines', 'columnspan','denomalign','depth','display','displaystyle','fence', 'frame','largeop','length','linethickness','lspace','lquote', 'mathbackground','mathcolor','mathsize','mathvariant','maxsize', 'minsize','movablelimits','notation','numalign','open','rowalign', 'rowlines','rowspacing','rowspan','rspace','rquote','scriptlevel', 'scriptminsize','scriptsizemultiplier','selection','separator', 'separators','stretchy','subscriptshift','supscriptshift','symmetric', 'voffset', // XML 'xlink:href','xml:id','xlink:title','xml:space','xmlns:xlink' ]); /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ var FORBID_TAGS = null; /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ var FORBID_ATTR = null; /* Decide if custom data attributes are okay */ var ALLOW_DATA_ATTR = true; /* Decide if unknown protocols are okay */ var ALLOW_UNKNOWN_PROTOCOLS = false; /* Output should be safe for jQuery's $() factory? */ var SAFE_FOR_JQUERY = false; /* Output should be safe for common template engines. * This means, DOMPurify removes data attributes, mustaches and ERB */ var SAFE_FOR_TEMPLATES = false; /* Specify template detection regex for SAFE_FOR_TEMPLATES mode */ var MUSTACHE_EXPR = /\{\{[\s\S]*|[\s\S]*\}\}/gm; var ERB_EXPR = /<%[\s\S]*|[\s\S]*%>/gm; /* Decide if document with ... should be returned */ var WHOLE_DOCUMENT = false; /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html string. * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead */ var RETURN_DOM = false; /* Decide if a DOM `DocumentFragment` should be returned, instead of a html string */ var RETURN_DOM_FRAGMENT = false; /* If `RETURN_DOM` or `RETURN_DOM_FRAGMENT` is enabled, decide if the returned DOM * `Node` is imported into the current `Document`. If this flag is not enabled the * `Node` will belong (its ownerDocument) to a fresh `HTMLDocument`, created by * DOMPurify. */ var RETURN_DOM_IMPORT = false; /* Output should be free from DOM clobbering attacks? */ var SANITIZE_DOM = true; /* Keep element content when removing element? */ var KEEP_CONTENT = true; /* Tags to ignore content of when KEEP_CONTENT is true */ var FORBID_CONTENTS = _addToSet({}, [ 'audio', 'head', 'math', 'script', 'style', 'svg', 'video' ]); /* Tags that are safe for data: URIs */ var DATA_URI_TAGS = _addToSet({}, [ 'audio', 'video', 'img', 'source' ]); /* Attributes safe for values like "javascript:" */ var URI_SAFE_ATTRIBUTES = _addToSet({}, [ 'alt','class','for','id','label','name','pattern','placeholder', 'summary','title','value','style','xmlns' ]); /* Keep a reference to config to pass to hooks */ var CONFIG = null; /* Ideally, do not touch anything below this line */ /* ______________________________________________ */ var formElement = document.createElement('form'); /** * _parseConfig * * @param optional config literal */ var _parseConfig = function(cfg) { /* Shield configuration object from tampering */ if (typeof cfg !== 'object') { cfg = {}; } /* Set configuration parameters */ ALLOWED_TAGS = 'ALLOWED_TAGS' in cfg ? _addToSet({}, cfg.ALLOWED_TAGS) : DEFAULT_ALLOWED_TAGS; ALLOWED_ATTR = 'ALLOWED_ATTR' in cfg ? _addToSet({}, cfg.ALLOWED_ATTR) : DEFAULT_ALLOWED_ATTR; FORBID_TAGS = 'FORBID_TAGS' in cfg ? _addToSet({}, cfg.FORBID_TAGS) : {}; FORBID_ATTR = 'FORBID_ATTR' in cfg ? _addToSet({}, cfg.FORBID_ATTR) : {}; ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false SAFE_FOR_JQUERY = cfg.SAFE_FOR_JQUERY || false; // Default false SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false RETURN_DOM = cfg.RETURN_DOM || false; // Default false RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false RETURN_DOM_IMPORT = cfg.RETURN_DOM_IMPORT || false; // Default false SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true if (SAFE_FOR_TEMPLATES) { ALLOW_DATA_ATTR = false; } if (RETURN_DOM_FRAGMENT) { RETURN_DOM = true; } /* Merge configuration parameters */ if (cfg.ADD_TAGS) { if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { ALLOWED_TAGS = _cloneObj(ALLOWED_TAGS); } _addToSet(ALLOWED_TAGS, cfg.ADD_TAGS); } if (cfg.ADD_ATTR) { if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { ALLOWED_ATTR = _cloneObj(ALLOWED_ATTR); } _addToSet(ALLOWED_ATTR, cfg.ADD_ATTR); } /* Add #text in case KEEP_CONTENT is set to true */ if (KEEP_CONTENT) { ALLOWED_TAGS['#text'] = true; } // Prevent further manipulation of configuration. // Not available in IE8, Safari 5, etc. if (Object && 'freeze' in Object) { Object.freeze(cfg); } CONFIG = cfg; }; /** * _forceRemove * * @param a DOM node */ var _forceRemove = function(node) { try { node.parentNode.removeChild(node); } catch (e) { node.outerHTML = ''; } }; /** * _initDocument * * @param a string of dirty markup * @return a DOM, filled with the dirty markup */ var _initDocument = function(dirty) { /* Create a HTML document using DOMParser */ var doc, body; try { doc = new DOMParser().parseFromString(dirty, 'text/html'); } catch (e) {} /* Some browsers throw, some browsers return null for the code above DOMParser with text/html support is only in very recent browsers. */ if (!doc) { doc = implementation.createHTMLDocument(''); body = doc.body; body.parentNode.removeChild(body.parentNode.firstElementChild); body.outerHTML = dirty; } /* Work on whole document or just its body */ if (typeof doc.getElementsByTagName === 'function') { return doc.getElementsByTagName( WHOLE_DOCUMENT ? 'html' : 'body')[0]; } return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0]; }; /** * _createIterator * * @param document/fragment to create iterator for * @return iterator instance */ var _createIterator = function(root) { return createNodeIterator.call(root.ownerDocument || root, root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT, function() { return NodeFilter.FILTER_ACCEPT; }, false ); }; /** * _isClobbered * * @param element to check for clobbering attacks * @return true if clobbered, false if safe */ var _isClobbered = function(elm) { if (elm instanceof Text || elm instanceof Comment) { return false; } if ( typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.removeChild !== 'function' || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== 'function' || typeof elm.setAttribute !== 'function' ) { return true; } return false; }; /** * _sanitizeElements * * @protect nodeName * @protect textContent * @protect removeChild * * @param node to check for permission to exist * @return true if node was killed, false if left alive */ var _sanitizeElements = function(currentNode) { var tagName, content; /* Execute a hook if present */ _executeHook('beforeSanitizeElements', currentNode, null); /* Check if element is clobbered or can clobber */ if (_isClobbered(currentNode)) { _forceRemove(currentNode); return true; } /* Now let's check the element's type and name */ tagName = currentNode.nodeName.toLowerCase(); /* Execute a hook if present */ _executeHook('uponSanitizeElement', currentNode, { tagName: tagName }); /* Remove element if anything forbids its presence */ if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { /* Keep content except for black-listed elements */ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName] && typeof currentNode.insertAdjacentHTML === 'function') { try { currentNode.insertAdjacentHTML('AfterEnd', currentNode.innerHTML); } catch (e) {} } _forceRemove(currentNode); return true; } /* Convert markup to cover jQuery behavior */ if (SAFE_FOR_JQUERY && !currentNode.firstElementChild && (!currentNode.content || !currentNode.content.firstElementChild)) { currentNode.innerHTML = currentNode.textContent.replace(/ tag that has an "id" // attribute at the time. if (lcName === 'name' && currentNode.nodeName === 'IMG' && attributes.id) { idAttr = attributes.id; attributes = Array.prototype.slice.apply(attributes); currentNode.removeAttribute('id'); currentNode.removeAttribute(name); if (attributes.indexOf(idAttr) > l) { currentNode.setAttribute('id', idAttr.value); } } else { // This avoids a crash in Safari v9.0 with double-ids. // The trick is to first set the id to be empty and then to // remove the attriubute if (name === 'id') { currentNode.setAttribute(name, ''); } currentNode.removeAttribute(name); } /* Did the hooks approve of the attribute? */ if (!hookEvent.keepAttr) { continue; } /* Make sure attribute cannot clobber */ if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in window || value in document || value in formElement)) { continue; } /* Sanitize attribute content to be template-safe */ if (SAFE_FOR_TEMPLATES) { value = value.replace(MUSTACHE_EXPR, ' '); value = value.replace(ERB_EXPR, ' '); } if ( /* Check the name is permitted */ (ALLOWED_ATTR[lcName] && !FORBID_ATTR[lcName] && ( /* Check no script, data or unknown possibly unsafe URI unless we know URI values are safe for that attribute */ URI_SAFE_ATTRIBUTES[lcName] || IS_ALLOWED_URI.test(value.replace(ATTR_WHITESPACE,'')) || /* Keep image data URIs alive if src is allowed */ (lcName === 'src' && value.indexOf('data:') === 0 && DATA_URI_TAGS[currentNode.nodeName.toLowerCase()]) )) || /* Allow potentially valid data-* attributes: * At least one character after "-" (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes) * XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804) * We don't need to check the value; it's always URI safe. */ (ALLOW_DATA_ATTR && DATA_ATTR.test(lcName)) || /* Allow unknown protocols: * This provides support for links that are handled by protocol handlers which may be unknown * ahead of time, e.g. fb:, spotify: */ (ALLOW_UNKNOWN_PROTOCOLS && !IS_SCRIPT_OR_DATA.test(value.replace(ATTR_WHITESPACE,''))) ) { /* Handle invalid data-* attribute set by try-catching it */ try { currentNode.setAttribute(name, value); } catch (e) {} } } /* Execute a hook if present */ _executeHook('afterSanitizeAttributes', currentNode, null); }; /** * _sanitizeShadowDOM * * @param fragment to iterate over recursively * @return void */ var _sanitizeShadowDOM = function(fragment) { var shadowNode; var shadowIterator = _createIterator(fragment); /* Execute a hook if present */ _executeHook('beforeSanitizeShadowDOM', fragment, null); while ( (shadowNode = shadowIterator.nextNode()) ) { /* Execute a hook if present */ _executeHook('uponSanitizeShadowNode', shadowNode, null); /* Sanitize tags and elements */ if (_sanitizeElements(shadowNode)) { continue; } /* Deep shadow DOM detected */ if (shadowNode.content instanceof DocumentFragment) { _sanitizeShadowDOM(shadowNode.content); } /* Check attributes, sanitize if necessary */ _sanitizeAttributes(shadowNode); } /* Execute a hook if present */ _executeHook('afterSanitizeShadowDOM', fragment, null); }; /** * _executeHook * Execute user configurable hooks * * @param {String} entryPoint Name of the hook's entry point * @param {Node} currentNode */ var _executeHook = function(entryPoint, currentNode, data) { if (!hooks[entryPoint]) { return; } hooks[entryPoint].forEach(function(hook) { hook.call(DOMPurify, currentNode, data, CONFIG); }); }; /** * sanitize * Public method providing core sanitation functionality * * @param {String} dirty string * @param {Object} configuration object */ DOMPurify.sanitize = function(dirty, cfg) { var body, currentNode, oldNode, nodeIterator, returnNode; /* Make sure we have a string to sanitize. DO NOT return early, as this will return the wrong type if the user has requested a DOM object rather than a string */ if (!dirty) { dirty = ''; } /* Stringify, in case dirty is an object */ if (typeof dirty !== 'string') { if (typeof dirty.toString !== 'function') { throw new TypeError('toString is not a function'); } else { dirty = dirty.toString(); } } /* Check we can run. Otherwise fall back or ignore */ if (!DOMPurify.isSupported) { if (typeof window.toStaticHTML === 'object' || typeof window.toStaticHTML === 'function') { return window.toStaticHTML(dirty); } return dirty; } /* Assign config vars */ _parseConfig(cfg); /* Exit directly if we have nothing to do */ if (!RETURN_DOM && !WHOLE_DOCUMENT && dirty.indexOf('<') === -1) { return dirty; } /* Initialize the document to work on */ body = _initDocument(dirty); /* Check we have a DOM node from the data */ if (!body) { return RETURN_DOM ? null : ''; } /* Get node iterator */ nodeIterator = _createIterator(body); /* Now start iterating over the created document */ while ( (currentNode = nodeIterator.nextNode()) ) { /* Fix IE's strange behavior with manipulated textNodes #89 */ if (currentNode.nodeType === 3 && currentNode === oldNode) { continue; } /* Sanitize tags and elements */ if (_sanitizeElements(currentNode)) { continue; } /* Shadow DOM detected, sanitize it */ if (currentNode.content instanceof DocumentFragment) { _sanitizeShadowDOM(currentNode.content); } /* Check attributes, sanitize if necessary */ _sanitizeAttributes(currentNode); oldNode = currentNode; } /* Return sanitized string or DOM */ if (RETURN_DOM) { if (RETURN_DOM_FRAGMENT) { returnNode = createDocumentFragment.call(body.ownerDocument); while (body.firstChild) { returnNode.appendChild(body.firstChild); } } else { returnNode = body; } if (RETURN_DOM_IMPORT) { /* adoptNode() is not used because internal state is not reset (e.g. the past names map of a HTMLFormElement), this is safe in theory but we would rather not risk another attack vector. The state that is cloned by importNode() is explicitly defined by the specs. */ returnNode = importNode.call(originalDocument, returnNode, true); } return returnNode; } return WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; }; /** * addHook * Public method to add DOMPurify hooks * * @param {String} entryPoint * @param {Function} hookFunction */ DOMPurify.addHook = function(entryPoint, hookFunction) { if (typeof hookFunction !== 'function') { return; } hooks[entryPoint] = hooks[entryPoint] || []; hooks[entryPoint].push(hookFunction); }; /** * removeHook * Public method to remove a DOMPurify hook at a given entryPoint * (pops it from the stack of hooks if more are present) * * @param {String} entryPoint * @return void */ DOMPurify.removeHook = function(entryPoint) { if (hooks[entryPoint]) { hooks[entryPoint].pop(); } }; /** * removeHooks * Public method to remove all DOMPurify hooks at a given entryPoint * * @param {String} entryPoint * @return void */ DOMPurify.removeHooks = function(entryPoint) { if (hooks[entryPoint]) { hooks[entryPoint] = []; } }; /** * removeAllHooks * Public method to remove all DOMPurify hooks * * @return void */ DOMPurify.removeAllHooks = function() { hooks = []; }; return DOMPurify; }));