You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
813 lines
30 KiB
JavaScript
813 lines
30 KiB
JavaScript
2 years ago
|
;(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 <html>... 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(/</g, '<');
|
||
|
}
|
||
|
|
||
|
/* Sanitize element content to be template-safe */
|
||
|
if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) {
|
||
|
/* Get the element's text content */
|
||
|
content = currentNode.textContent;
|
||
|
content = content.replace(MUSTACHE_EXPR, ' ');
|
||
|
content = content.replace(ERB_EXPR, ' ');
|
||
|
currentNode.textContent = content;
|
||
|
}
|
||
|
|
||
|
/* Execute a hook if present */
|
||
|
_executeHook('afterSanitizeElements', currentNode, null);
|
||
|
|
||
|
return false;
|
||
|
};
|
||
|
|
||
|
var DATA_ATTR = /^data-[\w.\u00B7-\uFFFF-]/;
|
||
|
var IS_ALLOWED_URI = /^(?:(?:(?:f|ht)tps?|mailto|tel):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
|
||
|
var IS_SCRIPT_OR_DATA = /^(?:\w+script|data):/i;
|
||
|
/* This needs to be extensive thanks to Webkit/Blink's behavior */
|
||
|
var ATTR_WHITESPACE = /[\x00-\x20\xA0\u1680\u180E\u2000-\u2029\u205f\u3000]/g;
|
||
|
|
||
|
/**
|
||
|
* _sanitizeAttributes
|
||
|
*
|
||
|
* @protect attributes
|
||
|
* @protect nodeName
|
||
|
* @protect removeAttribute
|
||
|
* @protect setAttribute
|
||
|
*
|
||
|
* @param node to sanitize
|
||
|
* @return void
|
||
|
*/
|
||
|
var _sanitizeAttributes = function(currentNode) {
|
||
|
var attr, name, value, lcName, idAttr, attributes, hookEvent, l;
|
||
|
/* Execute a hook if present */
|
||
|
_executeHook('beforeSanitizeAttributes', currentNode, null);
|
||
|
|
||
|
attributes = currentNode.attributes;
|
||
|
|
||
|
/* Check if we have attributes; if not we might have a text node */
|
||
|
if (!attributes) { return; }
|
||
|
|
||
|
hookEvent = {
|
||
|
attrName: '',
|
||
|
attrValue: '',
|
||
|
keepAttr: true
|
||
|
};
|
||
|
l = attributes.length;
|
||
|
|
||
|
/* Go backwards over all attributes; safely remove bad ones */
|
||
|
while (l--) {
|
||
|
attr = attributes[l];
|
||
|
name = attr.name;
|
||
|
value = attr.value;
|
||
|
lcName = name.toLowerCase();
|
||
|
|
||
|
/* Execute a hook if present */
|
||
|
hookEvent.attrName = lcName;
|
||
|
hookEvent.attrValue = value;
|
||
|
hookEvent.keepAttr = true;
|
||
|
_executeHook('uponSanitizeAttribute', currentNode, hookEvent );
|
||
|
value = hookEvent.attrValue;
|
||
|
|
||
|
/* Remove attribute */
|
||
|
// Safari (iOS + Mac), last tested v8.0.5, crashes if you try to
|
||
|
// remove a "name" attribute from an <img> 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;
|
||
|
}));
|