dompurify
Advanced tools
Comparing version 0.6.2 to 0.6.3
{ | ||
"name": "DOMPurify", | ||
"version": "0.6.2", | ||
"version": "0.6.3", | ||
"homepage": "https://github.com/cure53/DOMPurify", | ||
@@ -5,0 +5,0 @@ "author": "Cure53 <info@cure53.de>", |
@@ -15,3 +15,3 @@ { | ||
"description": "DOMPurify is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. It's written in JavaScript and works in all modern browsers (Safari, Opera (15+), Internet Explorer (10+), Firefox and Chrome - as well as almost anything else using Blink or WebKit). DOMPurify is written by security people who have vast background in web attacks and XSS. Fear not.", | ||
"version": "0.6.2", | ||
"version": "0.6.3", | ||
"main": "purify.js", | ||
@@ -18,0 +18,0 @@ "directories": { |
830
purify.js
@@ -19,470 +19,496 @@ /* jshint boss: true */ | ||
/** | ||
* Version label, exposed for easier checks | ||
* Version label, exposed for easier checks | ||
* if DOMPurfy is up to date or not | ||
*/ | ||
DOMPurify.version = '0.6.2'; | ||
DOMPurify.version = '0.6.3'; | ||
/** | ||
* sanitize | ||
* Public method providing core sanitation functionality | ||
* | ||
* @param {mixed} dirty string or DOM | ||
* @param {Object} configuration object | ||
* Expose whether this browser supports running the full DOMPurify. | ||
*/ | ||
DOMPurify.sanitize = function(dirty, cfg) { | ||
DOMPurify.isSupported = | ||
typeof document.implementation.createHTMLDocument !== 'undefined' && | ||
document.documentMode !== 9; | ||
/** | ||
* We consider the elements and attributes below to be safe. Ideally | ||
* don't add any new ones but feel free to remove unwanted ones. | ||
*/ | ||
/* Add properties to a lookup table */ | ||
var _addToSet = function(set, array) { | ||
var l = array.length; | ||
while (l--) { | ||
set[array[l]] = true; | ||
} | ||
return set; | ||
}; | ||
/* allowed element names */ | ||
var ALLOWED_TAGS = [ | ||
/* 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; | ||
}; | ||
// 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', | ||
/** | ||
* We consider the elements and attributes below to be safe. Ideally | ||
* don't add any new ones but feel free to remove unwanted ones. | ||
*/ | ||
// SVG | ||
'svg','altglyph','altglyphdef','altglyphitem','animatecolor', | ||
'animatemotion','animatetransform','circle','clippath','defs','desc', | ||
'ellipse','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', | ||
/* allowed element names */ | ||
var ALLOWED_TAGS = null; | ||
var DEFAULT_ALLOWED_TAGS = _addToSet({}, [ | ||
//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', | ||
// 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', | ||
//Text | ||
'#text' | ||
]; | ||
// SVG | ||
'svg','altglyph','altglyphdef','altglyphitem','animatecolor', | ||
'animatemotion','animatetransform','circle','clippath','defs','desc', | ||
'ellipse','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', | ||
/* Decide if custom data attributes are okay */ | ||
var ALLOW_DATA_ATTR = true; | ||
//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', | ||
/* Allowed attribute names */ | ||
var ALLOWED_ATTR = [ | ||
//Text | ||
'#text' | ||
]); | ||
// HTML | ||
'accept','action','align','alt','autocomplete','bgcolor','border', | ||
'checked','cite','class','color','cols','colspan','coords','datetime', | ||
'default','dir','disabled','download','enctype','for','headers','height', | ||
'hidden','high','href','hreflang','id','ismap','label','lang','list', | ||
'loop', 'low','max','maxlength','media','method','min','multiple', | ||
'name','novalidate','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', | ||
/* Allowed attribute names */ | ||
var ALLOWED_ATTR = null; | ||
var DEFAULT_ALLOWED_ATTR = _addToSet({}, [ | ||
// SVG | ||
'accent-height','accumulate','additivive','alignment-baseline', | ||
'ascent','azimuth','baseline-shift','bias','clip','clip-path', | ||
'clip-rule','color','color-interpolation','color-interpolation-filters', | ||
'color-profile','color-rendering','cx','cy','d','dy','dy','direction', | ||
'display','divisor','dur','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', | ||
'front-weight','image-rendering','in','in2','k1','k2','k3','k4','kerning', | ||
'letter-spacing','lighting-color','local','marker-end','marker-mid', | ||
'marker-start','max','mask','mode','min','operator','opacity','order', | ||
'overflow','paint-order','path','points','r','rx','ry','radius','restart', | ||
'scale','seed','shape-rendering','stop-color','stop-opacity', | ||
'stroke-dasharray','stroke-dashoffset','stroke-linecap','stroke-linejoin', | ||
'stroke-miterlimit','stroke-opacity','stroke','stroke-width','transform', | ||
'text-anchor','text-decoration','text-rendering','u1','u2','viewbox', | ||
'visibility','word-spacing','wrap','writing-mode','x','x1','x2','y', | ||
'y1','y2','z', | ||
// HTML | ||
'accept','action','align','alt','autocomplete','bgcolor','border', | ||
'checked','cite','class','color','cols','colspan','coords','datetime', | ||
'default','dir','disabled','download','enctype','for','headers','height', | ||
'hidden','high','href','hreflang','id','ismap','label','lang','list', | ||
'loop', 'low','max','maxlength','media','method','min','multiple', | ||
'name','novalidate','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', | ||
// 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', | ||
// SVG | ||
'accent-height','accumulate','additivive','alignment-baseline', | ||
'ascent','azimuth','baseline-shift','bias','clip','clip-path', | ||
'clip-rule','color','color-interpolation','color-interpolation-filters', | ||
'color-profile','color-rendering','cx','cy','d','dy','dy','direction', | ||
'display','divisor','dur','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', | ||
'front-weight','image-rendering','in','in2','k1','k2','k3','k4','kerning', | ||
'letter-spacing','lighting-color','local','marker-end','marker-mid', | ||
'marker-start','max','mask','mode','min','operator','opacity','order', | ||
'overflow','paint-order','path','points','r','rx','ry','radius','restart', | ||
'scale','seed','shape-rendering','stop-color','stop-opacity', | ||
'stroke-dasharray','stroke-dashoffset','stroke-linecap','stroke-linejoin', | ||
'stroke-miterlimit','stroke-opacity','stroke','stroke-width','transform', | ||
'text-anchor','text-decoration','text-rendering','u1','u2','viewbox', | ||
'visibility','word-spacing','wrap','writing-mode','x','x1','x2','y', | ||
'y1','y2','z', | ||
// XML | ||
'xlink:href','xml:id','xlink:title','xml:space' | ||
]; | ||
/* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ | ||
var FORBID_ATTR = []; | ||
/* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ | ||
var FORBID_TAGS = []; | ||
// 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', | ||
/* Decide if document with <html>... should be returned */ | ||
var WHOLE_DOCUMENT = false; | ||
// XML | ||
'xlink:href','xml:id','xlink:title','xml:space' | ||
]); | ||
/* Decide if a DOM node or a string should be returned */ | ||
var RETURN_DOM = false; | ||
/* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ | ||
var FORBID_TAGS = null; | ||
/* Output should be safe for jQuery's $() factory? */ | ||
var SAFE_FOR_JQUERY = false; | ||
/* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ | ||
var FORBID_ATTR = null; | ||
/* Output should be free from DOM clobbering attacks? */ | ||
var SANITIZE_DOM = true; | ||
/* Decide if custom data attributes are okay */ | ||
var ALLOW_DATA_ATTR = true; | ||
/* Keep element content when removing element? */ | ||
var KEEP_CONTENT = true; | ||
/* Output should be safe for jQuery's $() factory? */ | ||
var SAFE_FOR_JQUERY = false; | ||
/* Tags to keep content from (when KEEP_CONTENT is true) */ | ||
var CONTENT_TAGS = [ | ||
'a','abbr','acronym','address','article','aside','b','bdi','bdo', | ||
'big','blink','blockquote','caption','center','cite','code','col', | ||
'dd','del','details','dfn','dir','div','dl','dt','em','figcaption', | ||
'figure','footer','h1','h2','h3','h4','h5','h6','header','i','ins', | ||
'kbd','label','legend','li','main','mark','marquee','nav','ol', | ||
'output','p','pre','q','rp','rt','ruby','s','samp','section','small', | ||
'span','strike','strong','sub','summary','sup','table','tbody','td', | ||
'tfoot','th','thead','time','tr','tt','u','ul','var' | ||
]; | ||
/* Decide if document with <html>... should be returned */ | ||
var WHOLE_DOCUMENT = false; | ||
var DEBUG_OUTPUT = false; | ||
/* Decide if a DOM node or a string should be returned */ | ||
var RETURN_DOM = false; | ||
/* Ideally, do not touch anything below this line */ | ||
/* ______________________________________________ */ | ||
/* Output should be free from DOM clobbering attacks? */ | ||
var SANITIZE_DOM = true; | ||
/** | ||
* _parseConfig | ||
* | ||
* @param optional config literal | ||
*/ | ||
var _parseConfig = function(cfg) { | ||
/* Keep element content when removing element? */ | ||
var KEEP_CONTENT = true; | ||
/* Shield configuration object from tampering */ | ||
if (typeof cfg !== 'object') { | ||
cfg = {}; | ||
} | ||
/* Tags to keep content from (when KEEP_CONTENT is true) */ | ||
var CONTENT_TAGS = _addToSet({}, [ | ||
'a','abbr','acronym','address','article','aside','b','bdi','bdo', | ||
'big','blink','blockquote','caption','center','cite','code','col', | ||
'dd','del','details','dfn','dir','div','dl','dt','em','figcaption', | ||
'figure','footer','h1','h2','h3','h4','h5','h6','header','i','ins', | ||
'kbd','label','legend','li','main','mark','marquee','nav','ol', | ||
'output','p','pre','q','rp','rt','ruby','s','samp','section','small', | ||
'span','strike','strong','sub','summary','sup','table','tbody','td', | ||
'tfoot','th','thead','time','tr','tt','u','ul','var' | ||
]); | ||
/* Set configuration parameters */ | ||
'ALLOWED_ATTR' in cfg ? ALLOWED_ATTR = cfg.ALLOWED_ATTR : null; | ||
'ALLOWED_TAGS' in cfg ? ALLOWED_TAGS = cfg.ALLOWED_TAGS : null; | ||
'FORBID_ATTR' in cfg ? FORBID_ATTR = cfg.FORBID_ATTR : null; | ||
'FORBID_TAGS' in cfg ? FORBID_TAGS = cfg.FORBID_TAGS : null; | ||
'ALLOW_DATA_ATTR' in cfg ? ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR : null; | ||
'SAFE_FOR_JQUERY' in cfg ? SAFE_FOR_JQUERY = cfg.SAFE_FOR_JQUERY : null; | ||
'WHOLE_DOCUMENT' in cfg ? WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT : null; | ||
'RETURN_DOM' in cfg ? RETURN_DOM = cfg.RETURN_DOM : null; | ||
'SANITIZE_DOM' in cfg ? SANITIZE_DOM = cfg.SANITIZE_DOM : null; | ||
'KEEP_CONTENT' in cfg ? KEEP_CONTENT = cfg.KEEP_CONTENT : null; | ||
'DEBUG_OUTPUT' in cfg ? DEBUG_OUTPUT = cfg.DEBUG_OUTPUT : null; | ||
/* Keep a reference to config to pass to hooks */ | ||
var CONFIG = null; | ||
/* Merge configuration parameters */ | ||
cfg.ADD_ATTR ? ALLOWED_ATTR = ALLOWED_ATTR.concat(cfg.ADD_ATTR) : null; | ||
cfg.ADD_TAGS ? ALLOWED_TAGS = ALLOWED_TAGS.concat(cfg.ADD_TAGS) : null; | ||
/* Add #text in case KEEP_CONTENT is set to true */ | ||
KEEP_CONTENT ? ALLOWED_TAGS.push('#text') : null; | ||
/* Ideally, do not touch anything below this line */ | ||
/* ______________________________________________ */ | ||
// Prevent further manipulation of configuration. | ||
// Not available in IE8, Safari 5, etc. | ||
if (Object && 'freeze' in Object) { Object.freeze(cfg); } | ||
}; | ||
/** | ||
* _parseConfig | ||
* | ||
* @param optional config literal | ||
*/ | ||
var _parseConfig = function(cfg) { | ||
/* Shield configuration object from tampering */ | ||
if (typeof cfg !== 'object') { | ||
cfg = {}; | ||
} | ||
/** | ||
* _initDocument | ||
* | ||
* @param a string of dirty markup | ||
* @return a DOM, filled with the dirty markup | ||
*/ | ||
var _initDocument = function(dirty) { | ||
/* 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 | ||
SAFE_FOR_JQUERY = cfg.SAFE_FOR_JQUERY || false; // Default false | ||
WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false | ||
RETURN_DOM = cfg.RETURN_DOM || false; // Default false | ||
SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true | ||
KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true | ||
/* Exit directly if we have nothing to do */ | ||
if (typeof dirty === 'string' && dirty.indexOf('<') === -1) { | ||
return dirty; | ||
/* Merge configuration parameters */ | ||
if (cfg.ADD_TAGS) { | ||
if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { | ||
ALLOWED_TAGS = _cloneObj(ALLOWED_TAGS); | ||
} | ||
/* Create documents to map markup to */ | ||
var dom = document.implementation.createHTMLDocument(''); | ||
dom.body.parentNode.removeChild(dom.body.parentNode.firstElementChild); | ||
dom.body.outerHTML = dirty; | ||
/* Work on whole document or just its body */ | ||
var body = WHOLE_DOCUMENT ? dom.body.parentNode : dom.body; | ||
if ( | ||
!(dom.body instanceof HTMLBodyElement) || | ||
!(dom.body instanceof HTMLHtmlElement) | ||
) { | ||
var doc = (typeof HTMLTemplateElement === 'function') ? | ||
document.createElement('template').content.ownerDocument : | ||
document; | ||
var freshdom = doc.implementation.createHTMLDocument(''); | ||
body = WHOLE_DOCUMENT | ||
? freshdom.getElementsByTagName.call(dom,'html')[0] | ||
: freshdom.getElementsByTagName.call(dom,'body')[0]; | ||
_addToSet(ALLOWED_TAGS, cfg.ADD_TAGS); | ||
} | ||
if (cfg.ADD_ATTR) { | ||
if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { | ||
ALLOWED_ATTR = _cloneObj(ALLOWED_ATTR); | ||
} | ||
return body; | ||
}; | ||
_addToSet(ALLOWED_ATTR, cfg.ADD_ATTR); | ||
} | ||
/** | ||
* _createIterator | ||
* | ||
* @param document/fragment to create iterator for | ||
* @return iterator instance | ||
*/ | ||
var _createIterator = function(doc) { | ||
return document.createNodeIterator( | ||
doc, | ||
NodeFilter.SHOW_ELEMENT | ||
| NodeFilter.SHOW_COMMENT | ||
| NodeFilter.SHOW_TEXT, | ||
function() { return NodeFilter.FILTER_ACCEPT; }, | ||
false | ||
); | ||
}; | ||
/* Add #text in case KEEP_CONTENT is set to true */ | ||
if (KEEP_CONTENT) { ALLOWED_TAGS['#text'] = true; } | ||
/** | ||
* _isClobbered | ||
* | ||
* @param element to check for clobbering attacks | ||
* @return true if clobbered, false if safe | ||
*/ | ||
var _isClobbered = function(elm) { | ||
if (elm instanceof Text) { | ||
return false; | ||
} | ||
if ( | ||
(elm.children && !(elm.children instanceof HTMLCollection)) | ||
|| (elm.attributes instanceof HTMLCollection) | ||
|| (elm.attributes instanceof NodeList) | ||
|| (elm.insertAdjacentHTML && typeof elm.insertAdjacentHTML !== 'function') | ||
|| (elm.outerHTML && typeof elm.outerHTML !== 'string') | ||
|| typeof elm.nodeName !== 'string' | ||
|| typeof elm.textContent !== 'string' | ||
|| typeof elm.nodeType !== 'number' | ||
|| typeof elm.COMMENT_NODE !== 'number' | ||
|| typeof elm.setAttribute !== 'function' | ||
|| typeof elm.hasAttribute !== 'function' | ||
|| typeof elm.cloneNode !== 'function' | ||
|| typeof elm.removeAttributeNode !== 'function' | ||
|| typeof elm.removeChild !== 'function' | ||
|| typeof elm.attributes.item !== 'function' | ||
|| (elm.id === 'createElement' || elm.name === 'createElement') | ||
|| (elm.id === 'implementation' || elm.name === 'implementation') | ||
|| (elm.id === 'createNodeIterator' || elm.name === 'createNodeIterator') | ||
) { | ||
return true; | ||
} | ||
return false; | ||
}; | ||
// Prevent further manipulation of configuration. | ||
// Not available in IE8, Safari 5, etc. | ||
if (Object && 'freeze' in Object) { Object.freeze(cfg); } | ||
/** | ||
* _sanitizeElements | ||
* | ||
* @protect removeChild | ||
* @protect nodeType | ||
* @protect nodeName | ||
* @protect textContent | ||
* @protect outerHTML | ||
* @protect currentNode | ||
* @protect insertAdjacentHTML | ||
* | ||
* @param node to check for permission to exist | ||
* @return true if node was killed, false if left alive | ||
*/ | ||
var _sanitizeElements = function(currentNode) { | ||
CONFIG = cfg; | ||
}; | ||
/* Execute a hook if present */ | ||
_executeHook('beforeSanitizeElements', currentNode, null); | ||
/** | ||
* _initDocument | ||
* | ||
* @param a string of dirty markup | ||
* @return a DOM, filled with the dirty markup | ||
*/ | ||
var _initDocument = function(dirty) { | ||
/* Create documents to map markup to */ | ||
var dom = document.implementation.createHTMLDocument(''); | ||
var freshdom, doc; | ||
/* Check if element is clobbered or can clobber */ | ||
if (_isClobbered(currentNode)) { | ||
/* Set content */ | ||
var body = dom.body; | ||
body.parentNode.removeChild(body.parentNode.firstElementChild); | ||
body.outerHTML = dirty; | ||
/* Be harsh with clobbered content, element has to go! */ | ||
try{ | ||
currentNode.parentNode.removeChild(currentNode); | ||
} catch(e){ | ||
currentNode.outerHTML=''; | ||
} | ||
return true; | ||
} | ||
/* Work on whole document or just its body */ | ||
body = WHOLE_DOCUMENT ? dom.body.parentNode : dom.body; | ||
if (!(body instanceof (WHOLE_DOCUMENT ? HTMLHtmlElement : HTMLBodyElement))) { | ||
doc = (typeof HTMLTemplateElement === 'function') ? | ||
document.createElement('template').content.ownerDocument : | ||
document; | ||
freshdom = doc.implementation.createHTMLDocument(''); | ||
body = WHOLE_DOCUMENT ? | ||
freshdom.getElementsByTagName.call(dom,'html')[0] : | ||
freshdom.getElementsByTagName.call(dom,'body')[0]; | ||
} | ||
return body; | ||
}; | ||
/* Now let's check the element's type and name */ | ||
var tagName = currentNode.nodeName.toLowerCase(); | ||
/** | ||
* _createIterator | ||
* | ||
* @param document/fragment to create iterator for | ||
* @return iterator instance | ||
*/ | ||
var _createIterator = function(doc) { | ||
return document.createNodeIterator( | ||
doc, | ||
NodeFilter.SHOW_ELEMENT | ||
| NodeFilter.SHOW_COMMENT | ||
| NodeFilter.SHOW_TEXT, | ||
function() { return NodeFilter.FILTER_ACCEPT; }, | ||
false | ||
); | ||
}; | ||
/* Execute a hook if present */ | ||
_executeHook('uponSanitizeElement', currentNode, { | ||
tagName: tagName | ||
}); | ||
/** | ||
* _isClobbered | ||
* | ||
* @param element to check for clobbering attacks | ||
* @return true if clobbered, false if safe | ||
*/ | ||
var _isClobbered = function(elm) { | ||
if (elm instanceof Text) { | ||
return false; | ||
} | ||
if ( | ||
(elm.children && !(elm.children instanceof HTMLCollection)) | ||
|| (elm.attributes instanceof HTMLCollection) | ||
|| (elm.attributes instanceof NodeList) | ||
|| (elm.insertAdjacentHTML && typeof elm.insertAdjacentHTML !== 'function') | ||
|| (elm.outerHTML && typeof elm.outerHTML !== 'string') | ||
|| typeof elm.nodeName !== 'string' | ||
|| typeof elm.textContent !== 'string' | ||
|| typeof elm.nodeType !== 'number' | ||
|| typeof elm.COMMENT_NODE !== 'number' | ||
|| typeof elm.setAttribute !== 'function' | ||
|| typeof elm.hasAttribute !== 'function' | ||
|| typeof elm.cloneNode !== 'function' | ||
|| typeof elm.removeAttributeNode !== 'function' | ||
|| typeof elm.removeChild !== 'function' | ||
|| typeof elm.attributes.item !== 'function' | ||
|| (elm.id === 'createElement' || elm.name === 'createElement') | ||
|| (elm.id === 'implementation' || elm.name === 'implementation') | ||
|| (elm.id === 'createNodeIterator' || elm.name === 'createNodeIterator') | ||
) { | ||
return true; | ||
} | ||
return false; | ||
}; | ||
if (currentNode.nodeType === currentNode.COMMENT_NODE | ||
|| ALLOWED_TAGS.indexOf(tagName) === -1 | ||
|| FORBID_TAGS.indexOf(tagName) > -1 | ||
) { | ||
/* Keep content for white-listed elements */ | ||
if (KEEP_CONTENT && currentNode.insertAdjacentHTML | ||
&& currentNode.nodeName.toLowerCase | ||
&& CONTENT_TAGS.indexOf(tagName) !== -1){ | ||
try { | ||
currentNode.insertAdjacentHTML('AfterEnd', currentNode.innerHTML); | ||
} catch(e) {} | ||
} | ||
/** | ||
* _sanitizeElements | ||
* | ||
* @protect removeChild | ||
* @protect nodeType | ||
* @protect nodeName | ||
* @protect textContent | ||
* @protect outerHTML | ||
* @protect currentNode | ||
* @protect insertAdjacentHTML | ||
* | ||
* @param node to check for permission to exist | ||
* @return true if node was killed, false if left alive | ||
*/ | ||
var _sanitizeElements = function(currentNode) { | ||
/* Execute a hook if present */ | ||
_executeHook('beforeSanitizeElements', currentNode, null); | ||
/* Remove element if anything permits its presence */ | ||
/* Check if element is clobbered or can clobber */ | ||
if (_isClobbered(currentNode)) { | ||
/* Be harsh with clobbered content, element has to go! */ | ||
try { | ||
currentNode.parentNode.removeChild(currentNode); | ||
return true; | ||
} catch (e) { | ||
currentNode.outerHTML = ''; | ||
} | ||
return true; | ||
} | ||
/* Finally, convert markup to cover jQuery behavior */ | ||
if (SAFE_FOR_JQUERY && !currentNode.firstElementChild) { | ||
currentNode.innerHTML = currentNode.textContent.replace(/</g, '<'); | ||
/* Now let's check the element's type and name */ | ||
var 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 for white-listed elements */ | ||
if (KEEP_CONTENT && currentNode.insertAdjacentHTML | ||
&& CONTENT_TAGS[tagName]){ | ||
try { | ||
currentNode.insertAdjacentHTML('AfterEnd', currentNode.innerHTML); | ||
} catch (e) {} | ||
} | ||
currentNode.parentNode.removeChild(currentNode); | ||
return true; | ||
} | ||
/* Execute a hook if present */ | ||
_executeHook('afterSanitizeElements', currentNode, null); | ||
/* Finally, convert markup to cover jQuery behavior */ | ||
if (SAFE_FOR_JQUERY && !currentNode.firstElementChild) { | ||
currentNode.innerHTML = currentNode.textContent.replace(/</g, '<'); | ||
} | ||
return false; | ||
}; | ||
/* Execute a hook if present */ | ||
_executeHook('afterSanitizeElements', currentNode, null); | ||
/** | ||
* _sanitizeAttributes | ||
* | ||
* @protect attributes | ||
* @protect removeAttribiuteNode | ||
* @protect setAttribute | ||
* @protect cloneNode | ||
* | ||
* @param node to sanitize | ||
* @return void | ||
*/ | ||
var _sanitizeAttributes = function(currentNode) { | ||
/* Execute a hook if present */ | ||
_executeHook('beforeSanitizeAttributes', currentNode, null); | ||
var regex = /^(\w+script|data):/gi, | ||
clonedNode = currentNode.cloneNode(true), | ||
tmp, clobbering; | ||
return false; | ||
}; | ||
/* This needs to be extensive thanks to Webkit/Blink's behavior */ | ||
var whitespace = /[\x00-\x20\xA0\u1680\u180E\u2000-\u2029\u205f\u3000]/g; | ||
/** | ||
* _sanitizeAttributes | ||
* | ||
* @protect attributes | ||
* @protect removeAttributeNode | ||
* @protect setAttribute | ||
* @protect cloneNode | ||
* | ||
* @param node to sanitize | ||
* @return void | ||
*/ | ||
var _sanitizeAttributes = function(currentNode) { | ||
/* Execute a hook if present */ | ||
_executeHook('beforeSanitizeAttributes', currentNode, null); | ||
/* Check if we have attributes; if not we might have a text node */ | ||
if (!currentNode.attributes) { return; } | ||
var isScriptOrData = /^(?:\w+script|data):/gi, | ||
clonedNode = currentNode.cloneNode(false), | ||
tmp, clobbering; | ||
/* Go backwards over all attributes; safely remove bad ones */ | ||
for (var attr = currentNode.attributes.length-1; attr >= 0; attr--) { | ||
/* This needs to be extensive thanks to Webkit/Blink's behavior */ | ||
var whitespace = /[\x00-\x20\xA0\u1680\u180E\u2000-\u2029\u205f\u3000]/g; | ||
tmp = clonedNode.attributes[attr]; | ||
clobbering = false; | ||
currentNode.removeAttribute(currentNode.attributes[attr].name); | ||
/* Check if we have attributes; if not we might have a text node */ | ||
if (!currentNode.attributes) { return; } | ||
if (!tmp instanceof Attr) { continue; } | ||
/* Go backwards over all attributes; safely remove bad ones */ | ||
for (var attr = currentNode.attributes.length-1; attr >= 0; attr--) { | ||
tmp = clonedNode.attributes[attr]; | ||
clobbering = false; | ||
currentNode.removeAttribute(currentNode.attributes[attr].name); | ||
if(SANITIZE_DOM) { | ||
if((tmp.name === 'id' || tmp.name === 'name') | ||
&& (tmp.value in window || tmp.value in document)) { | ||
clobbering = true; | ||
} | ||
if (!tmp instanceof Attr) { continue; } | ||
if (SANITIZE_DOM) { | ||
if ((tmp.name === 'id' || tmp.name === 'name') | ||
&& (tmp.value in window || tmp.value in document)) { | ||
clobbering = true; | ||
} | ||
/* Safely handle attributes */ | ||
var attrName = tmp.name.toLowerCase(); | ||
} | ||
/* Safely handle attributes */ | ||
var attrName = tmp.name.toLowerCase(); | ||
/* Execute a hook if present */ | ||
_executeHook('uponSanitizeAttribute', currentNode, { | ||
attrName: attrName, attrValue: tmp.value, attr: tmp | ||
}); | ||
/* Execute a hook if present */ | ||
_executeHook('uponSanitizeAttribute', currentNode, { | ||
attrName: attrName, attrValue: tmp.value, attr: tmp | ||
}); | ||
if ( | ||
((ALLOWED_ATTR.indexOf(attrName) > -1 && | ||
FORBID_ATTR.indexOf(attrName) === -1) || | ||
(ALLOW_DATA_ATTR && tmp.name.match(/^data-[\w-]+/i))) | ||
if ( | ||
((ALLOWED_ATTR[attrName] && !FORBID_ATTR[attrName]) || | ||
(ALLOW_DATA_ATTR && /^data-[\w-]+/i.test(tmp.name))) | ||
/* Get rid of script and data URIs */ | ||
&& (!tmp.value.replace(whitespace,'').match(regex) | ||
/* Get rid of script and data URIs */ | ||
&& (!isScriptOrData.test(tmp.value.replace(whitespace,'')) | ||
/* Keep image data URIs alive if src is allowed */ | ||
|| (tmp.name === 'src' | ||
&& tmp.value.indexOf('data:') === 0 | ||
&& currentNode.nodeName === 'IMG')) | ||
/* Keep image data URIs alive if src is allowed */ | ||
|| (tmp.name === 'src' | ||
&& tmp.value.indexOf('data:') === 0 | ||
&& currentNode.nodeName === 'IMG')) | ||
/* Make sure attribute cannot clobber */ | ||
&& !clobbering | ||
) { | ||
/* Handle invalid data attribute set by try-catching it */ | ||
try { | ||
currentNode.setAttribute(tmp.name, tmp.value); | ||
} catch (e) {} | ||
} | ||
/* Make sure attribute cannot clobber */ | ||
&& !clobbering | ||
) { | ||
/* Handle invalid data attribute set by try-catching it */ | ||
try { | ||
currentNode.setAttribute(tmp.name, tmp.value); | ||
} catch (e) {} | ||
} | ||
} | ||
/* Execute a hook if present */ | ||
_executeHook('afterSanitizeAttributes', currentNode, null); | ||
}; | ||
/* 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); | ||
/** | ||
* _sanitizeShadowDOM | ||
* | ||
* @param fragment to iterate over recursively | ||
* @return void | ||
*/ | ||
var _sanitizeShadowDOM = function(fragment) { | ||
var shadowNode; | ||
var shadowIterator = _createIterator(fragment); | ||
while (shadowNode = shadowIterator.nextNode()) { | ||
/* Execute a hook if present */ | ||
_executeHook('uponSanitizeShadowNode', shadowNode, null); | ||
/* Execute a hook if present */ | ||
_executeHook('beforeSanitizeShadowDOM', fragment, null); | ||
/* Sanitize tags and elements */ | ||
if (_sanitizeElements(shadowNode)) { | ||
continue; | ||
} | ||
while (shadowNode = shadowIterator.nextNode()) { | ||
/* Execute a hook if present */ | ||
_executeHook('uponSanitizeShadowNode', shadowNode, null); | ||
/* Deep shadow DOM detected */ | ||
if (shadowNode.content instanceof DocumentFragment) { | ||
_sanitizeShadowDOM(shadowNode.content); | ||
} | ||
/* Sanitize tags and elements */ | ||
if (_sanitizeElements(shadowNode)) { | ||
continue; | ||
} | ||
/* Check attributes, sanitize if necessary */ | ||
_sanitizeAttributes(shadowNode); | ||
/* Deep shadow DOM detected */ | ||
if (shadowNode.content instanceof DocumentFragment) { | ||
_sanitizeShadowDOM(shadowNode.content); | ||
} | ||
/* 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; } | ||
/* Check attributes, sanitize if necessary */ | ||
_sanitizeAttributes(shadowNode); | ||
} | ||
hooks[entryPoint].forEach(function(hook) { | ||
hook.call(DOMPurify, currentNode, data, cfg); | ||
}); | ||
}; | ||
/* Execute a hook if present */ | ||
_executeHook('afterSanitizeShadowDOM', fragment, null); | ||
}; | ||
/* Feature check and untouched opt-out return */ | ||
if (typeof document.implementation.createHTMLDocument === 'undefined' | ||
|| (typeof document.documentMode === 'number' && document.documentMode === 9)) { | ||
/** | ||
* _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) { | ||
/* Check we can run. Otherwise fall back or ignore */ | ||
if (!DOMPurify.isSupported) { | ||
if (typeof window.toStaticHTML === 'function' && typeof dirty === 'string') { | ||
@@ -495,10 +521,15 @@ return window.toStaticHTML(dirty); | ||
/* Assign config vars */ | ||
cfg ? _parseConfig(cfg) : null; | ||
_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 */ | ||
var body = _initDocument(dirty); | ||
/* Early exit in case document is empty */ | ||
if (typeof body !== 'object') { | ||
return body ? body : ''; | ||
/* Check we have a DOM node from the data */ | ||
if (!body) { | ||
return RETURN_DOM ? null : ''; | ||
} | ||
@@ -541,3 +572,2 @@ | ||
DOMPurify.addHook = function(entryPoint, hookFunction) { | ||
if (typeof hookFunction !== 'function') { return; } | ||
@@ -544,0 +574,0 @@ hooks[entryPoint] = hooks[entryPoint] || []; |
@@ -162,2 +162,2 @@ # DOMPurify [![NPM version](http://img.shields.io/npm/v/dompurify.svg)](https://www.npmjs.org/package/dompurify) | ||
Big thanks also go to [@asutherland](https://twitter.com/asutherland), [@mathias](https://twitter.com/mathias), [@cgvwzq](https://twitter.com/cgvwzq), [@robbertatwork](https://twitter.com/robbertatwork), [@giutro](https://twitter.com/giutro) and [@fhemberger](https://twitter.com/fhemberger)! | ||
Big thanks also go to [@asutherland](https://twitter.com/asutherland), [@mathias](https://twitter.com/mathias), [@cgvwzq](https://twitter.com/cgvwzq), [@robbertatwork](https://twitter.com/robbertatwork), [@giutro](https://twitter.com/giutro) and [@fhemberger](https://twitter.com/fhemberger)! Further, thanks [@neilj](https://twitter.com/neilj) for his code review and countless small optimizations, fixes and beautifications. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
10768
1
734748