From af8a8ffcc3bbd2b3f62e5f43eee6287e6c16e21e Mon Sep 17 00:00:00 2001 From: Frederic Hemberger Date: Sat, 1 Mar 2014 11:31:46 +0100 Subject: [PATCH 1/4] Basic JSHint setup and fixes Also nobody ever died from too much white space. --- .jshintrc | 16 +++++ purify.js | 208 ++++++++++++++++++++++++++++-------------------------- 2 files changed, 125 insertions(+), 99 deletions(-) create mode 100644 .jshintrc diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 000000000..4531038ec --- /dev/null +++ b/.jshintrc @@ -0,0 +1,16 @@ +{ + "browser": true, + "curly": true, + "eqeqeq": true, + "expr": true, + "laxbreak": true, + "smarttabs": true, + "strict": true, + "undef": true, + "unused": true, + "globals": { + "Attr": false, + "DocumentFragment": false, + "HTMLCollection": false + } +} diff --git a/purify.js b/purify.js index 5ba5db6bc..932e9862b 100644 --- a/purify.js +++ b/purify.js @@ -1,8 +1,8 @@ /* BOF */ ; -DOMPurify = {}; -DOMPurify.sanitize = function(dirty, cfg){ - +var DOMPurify = {}; +DOMPurify.sanitize = function(dirty, cfg) { + /******* /*************************************************************** ****** / * Don't allow script elements. Or event handlers. * ***** / * And careful with SVG's "values" attribute. * @@ -12,7 +12,7 @@ DOMPurify.sanitize = function(dirty, cfg){ * / /* allowed element names */ var ALLOWED_TAGS = [ - + // HTML 'a','abbr','acronym','address','area','article','aside','audio','b', 'bdi','bdo','big','blink','blockquote','body','br','button','canvas', @@ -27,7 +27,7 @@ DOMPurify.sanitize = function(dirty, cfg){ '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', @@ -35,20 +35,20 @@ DOMPurify.sanitize = function(dirty, cfg){ 'lineargradient','marker','mask','metadata','mpath','path','pattern', 'polygon','polyline','radialgradient','rect','stop','switch','symbol', 'text','textpath','title','tref','tspan','view','vkern', - + //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' ]; - + /* Decide if custom data attributes are okay */ var ALLOW_DATA_ATTR = true; - - /* Allowed attribute names */ + + /* Allowed attribute names */ var ALLOWED_ATTR = [ - + // HTML 'name', 'id','href','action','class','title','alt','src', 'type', 'height','width', 'method','rev','rel','accept','align','autocomplete', @@ -60,13 +60,13 @@ DOMPurify.sanitize = function(dirty, cfg){ 'preload','pubdate','radiogroup','readonly','required','reversed', 'rows','rowspan','spellcheck','scope','selected','shape','size','span', 'srclang','start','step','style','summary','tabindex','usemap','value', - + // SVG 'wrap','clip','cx','cy','d','dy','dy','in','in2','k1','k2','k3','k4', 'mask','mode','opacity','order','overflow','path','points','radius', 'rx','ry','scale','stroke','stroke-width','transform','u1','u2','r','x', 'y','x1','viewbox','x2','y1','y2','z','fill', - + // MathML 'accent','accentunder','bevelled','close','columnsalign','columnlines', 'columnspan','denomalign','depth','display','displaystyle','fence', @@ -78,58 +78,60 @@ DOMPurify.sanitize = function(dirty, cfg){ 'separators','stretchy','subscriptshift','supscriptshift','symmetric', 'voffset' ]; - + /* Decide if document with ... should be returned */ var WHOLE_DOCUMENT = false; - + /* Decide if a DOM node or a string should be returned */ var RETURN_DOM = false; - + /* Output should be safe for jQuery's $() factory? */ - var SAFE_FOR_JQUERY = false; - + var SAFE_FOR_JQUERY = false; + /* Ideally, do not touch anything below this line */ /* ______________________________________________ */ /** * _parseConfig - * + * * @param optional config literal */ - var _parseConfig = function(cfg){ + var _parseConfig = function(cfg) { cfg.ALLOWED_ATTR ? ALLOWED_ATTR = cfg.ALLOWED_ATTR : null; cfg.ALLOWED_TAGS ? ALLOWED_TAGS = cfg.ALLOWED_TAGS : null; cfg.ALLOW_DATA_ATTR ? ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR : null; - cfg.SAFE_FOR_JQUERY ? SAFE_FOR_JQUERY = cfg.SAFE_FOR_JQUERY : null; + cfg.SAFE_FOR_JQUERY ? SAFE_FOR_JQUERY = cfg.SAFE_FOR_JQUERY : null; cfg.WHOLE_DOCUMENT ? WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT : null; - cfg.RETURN_DOM ? RETURN_DOM = cfg.RETURN_DOM : null; - } - - + cfg.RETURN_DOM ? RETURN_DOM = cfg.RETURN_DOM : null; + }; + + /** * _createIterator - * + * * @param document/fragment to create iterator for * @return iterator instance */ - var _createIterator = function(doc){ + var _createIterator = function(doc) { return document.createNodeIterator( doc, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT, - function() { return NodeFilter.FILTER_ACCEPT }, false - ); - } + 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.children && !(elm.children instanceof HTMLCollection)) + var _isClobbered = function(elm) { + if ( + (elm.children && !(elm.children instanceof HTMLCollection)) || typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.nodeType !== 'number' @@ -137,138 +139,146 @@ DOMPurify.sanitize = function(dirty, cfg){ || typeof elm.setAttribute !== 'function' || typeof elm.cloneNode !== 'function' || typeof elm.removeAttributeNode !== 'function' - || typeof elm.attributes.item !== 'function'){ + || typeof elm.attributes.item !== 'function' + ) { return true; } return false; - } - - + }; + + /** * _sanitizeElements - * + * * @protect removeChild * @protect nodeType * @protect nodeName - * @protect textContent - * @protect currentNode - * + * @protect textContent + * @protect currentNode + * * @param node to check for permission to exist * @return true if node was killed, false if left alive */ - var _sanitizeElements = function(currentNode){ - if(_isClobbered(currentNode) || currentNode.nodeType === currentNode.COMMENT_NODE - || ALLOWED_TAGS.indexOf(currentNode.nodeName.toLowerCase()) === -1) { + var _sanitizeElements = function(currentNode) { + if ( + _isClobbered(currentNode) + || currentNode.nodeType === currentNode.COMMENT_NODE + || ALLOWED_TAGS.indexOf(currentNode.nodeName.toLowerCase()) === -1 + ) { currentNode.parentNode.removeChild(currentNode); return true; } - if(SAFE_FOR_JQUERY && !currentNode.firstElementChild){ - currentNode.textContent - = currentNode.textContent.replace(/=0; attr--){ - var tmp = clonedNode.attributes[attr]; + + for (var attr = currentNode.attributes.length-1; attr >= 0; attr--) { + var tmp = clonedNode.attributes[attr]; currentNode.removeAttribute(currentNode.attributes[attr].name); - if(tmp instanceof Attr) { - if((ALLOWED_ATTR.indexOf(tmp.name.toLowerCase()) > -1 - || (ALLOW_DATA_ATTR && tmp.name.match(/^data-[\w-]+/i))) - && !tmp.value.replace(/[\x00-\x20]/g,'').match(regex)) { - if(tmp.name === 'style' && currentNode.style.cssText){ - currentNode.setAttribute( - 'style', currentNode.style.cssText); - } else { - currentNode.setAttribute(tmp.name, tmp.value); - } + + if (tmp instanceof Attr) { + if ( + (ALLOWED_ATTR.indexOf(tmp.name.toLowerCase()) > -1 || + (ALLOW_DATA_ATTR && tmp.name.match(/^data-[\w-]+/i))) + && !tmp.value.replace(/[\x00-\x20]/g,'').match(regex) + ) { + currentNode.setAttribute(tmp.name, tmp.value); } } - } - } + } + }; + - /** * _sanitizeShadowDOM - * + * * @param fragment to iterate over recursively * @return void */ - var _sanitizeShadowDOM = function(fragment){ - var shadowNode; + var _sanitizeShadowDOM = function(fragment) { + var shadowNode; var shadowIterator = _createIterator(fragment); - while(shadowNode = shadowIterator.nextNode()) { + + while (shadowNode = shadowIterator.nextNode()) { /* Sanitize tags and elements */ - if(_sanitizeElements(shadowNode)){ + if (_sanitizeElements(shadowNode)) { continue; } + /* Deep shadow DOM detected */ - if(shadowNode.content instanceof DocumentFragment){ + if (shadowNode.content instanceof DocumentFragment) { _sanitizeShadowDOM(shadowNode.content); - } + /* Check attributes, sanitize if necessary */ _sanitizeAttributes(shadowNode); - } - } - - + } + }; + + /* Assign config vars */ cfg ? _parseConfig(cfg) : null; - - + + /* Create documents to map markup to */ var dom = document.implementation.createHTMLDocument(''); dom.body.parentNode.removeChild(dom.body.parentNode.firstElementChild); - dom.body.outerHTML=dirty; - var body = WHOLE_DOCUMENT ? dom.body.parentNode : dom.body; - - if(!(dom.body instanceof HTMLBodyElement) - || !(dom.body instanceof HTMLHtmlElement)){ - var freshdom = document.implementation.createHTMLDocument(''); - body = WHOLE_DOCUMENT - ? freshdom.getElementsByTagName.call(dom,'html')[0] + dom.body.outerHTML = dirty; + var body = WHOLE_DOCUMENT ? dom.body.parentNode : dom.body; + + if ( + !(dom.body instanceof HTMLBodyElement) || + !(dom.body instanceof HTMLHtmlElement) + ) { + var freshdom = document.implementation.createHTMLDocument(''); + body = WHOLE_DOCUMENT + ? freshdom.getElementsByTagName.call(dom,'html')[0] : freshdom.getElementsByTagName.call(dom,'body')[0]; } - - + + /* Get node iterator */ - var currentNode; + var currentNode; var nodeIterator = _createIterator(body); - - + + /* Now start iterating over the created document */ - while(currentNode = nodeIterator.nextNode()) { + while (currentNode = nodeIterator.nextNode()) { /* Sanitize tags and elements */ - if(_sanitizeElements(currentNode)){ + if (_sanitizeElements(currentNode)) { continue; } + /* Shadow DOM detected, sanitize it */ - if(currentNode.content instanceof DocumentFragment){ + if (currentNode.content instanceof DocumentFragment) { _sanitizeShadowDOM(currentNode.content); } + /* Check attributes, sanitize if necessary */ _sanitizeAttributes(currentNode); } - /* Return sanitized string or DOM */ - if(RETURN_DOM){ + /* Return sanitized string or DOM */ + if (RETURN_DOM) { return body; } return WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; From 5887020f68601075584a50d5f4fc18502b142af4 Mon Sep 17 00:00:00 2001 From: Frederic Hemberger Date: Sat, 1 Mar 2014 11:34:19 +0100 Subject: [PATCH 2/4] Remove IDE specific file from repository --- .project | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .project diff --git a/.project b/.project deleted file mode 100644 index 02fff07bc..000000000 --- a/.project +++ /dev/null @@ -1,11 +0,0 @@ - - - DOMPurify - - - - - - - - From f2924ae2033568a0435b5edc8114745e0a3ee77d Mon Sep 17 00:00:00 2001 From: Frederic Hemberger Date: Sat, 1 Mar 2014 11:39:42 +0100 Subject: [PATCH 3/4] Typos --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 985590813..8cafff9ef 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ DOMPurify sanitizes HTML and prevents XSS attacks. You can feed DOMPurify with s ### How do I use it? -It's easy. Just include DOMPurify on your website. Afterwards you can sanitiize strings by executing the following code: +It's easy. Just include DOMPurify on your website. Afterwards you can sanitize strings by executing the following code: ```javascript var clean = DOMPurify.sanitize(dirty); @@ -25,7 +25,7 @@ DOMPurify currently supports HTML5, SVG and MathML. DOMPurify per default allows ### Can I configure it? -Yes. The included default configuartion values are pretty good already - but you can of course override them: +Yes. The included default configuration values are pretty good already - but you can of course override them: ```javascript // Allow only From 98f3b67dc6a9158bdd7cbe2e98aeed603287dae3 Mon Sep 17 00:00:00 2001 From: Frederic Hemberger Date: Sat, 1 Mar 2014 12:14:23 +0100 Subject: [PATCH 4/4] Add AMD support --- .jshintrc | 1 + README.md | 9 + purify.js | 532 ++++++++++++++++++++++++++++-------------------------- 3 files changed, 282 insertions(+), 260 deletions(-) diff --git a/.jshintrc b/.jshintrc index 4531038ec..0d810eec7 100644 --- a/.jshintrc +++ b/.jshintrc @@ -10,6 +10,7 @@ "unused": true, "globals": { "Attr": false, + "define": false, "DocumentFragment": false, "HTMLCollection": false } diff --git a/README.md b/README.md index 8cafff9ef..d253a19d4 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,15 @@ It's easy. Just include DOMPurify on your website. Afterwards you can sanitize s var clean = DOMPurify.sanitize(dirty); ``` +If you're using an [AMD](https://github.com/amdjs/amdjs-api/wiki/AMD) module loader like [Require.js](http://requirejs.org/), you can load this script asynchronous as well: + +```javascript +require(['dompurify'], function(DOMPurify) { + var clean = DOMPurify.sanitize(dirty); +}; +``` + + ### Is there a demo? Of course there is a demo! [Play with DOMPurify](https://cure53.de/purify) diff --git a/purify.js b/purify.js index 932e9862b..2432c793c 100644 --- a/purify.js +++ b/purify.js @@ -1,286 +1,298 @@ -/* BOF */ -; -var DOMPurify = {}; -DOMPurify.sanitize = function(dirty, cfg) { - - /******* /*************************************************************** - ****** / * Don't allow script elements. Or event handlers. * - ***** / * And careful with SVG's "values" attribute. * - **** / * Other than that, you can basically allow whatever you want. * - *** / * We'll make it safe for you. * - ** / ******************************************************************* - * / - /* allowed element names */ - var ALLOWED_TAGS = [ - - // 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','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', - - //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' - ]; - - /* Decide if custom data attributes are okay */ - var ALLOW_DATA_ATTR = true; - - /* Allowed attribute names */ - var ALLOWED_ATTR = [ - - // HTML - 'name', 'id','href','action','class','title','alt','src', 'type', - 'height','width', 'method','rev','rel','accept','align','autocomplete', - 'xmlns','bgcolor','border','checked','cite','color','cols','colspan', - 'coords','datetime','default','dir','disabled','download','enctype', - 'for','headers','hidden','high','hreflang','ismap','label','lang', - 'list','loop', 'low','max','maxlength','media','min','multiple', - 'novalidate','open','optimum','pattern','placeholder','poster', - 'preload','pubdate','radiogroup','readonly','required','reversed', - 'rows','rowspan','spellcheck','scope','selected','shape','size','span', - 'srclang','start','step','style','summary','tabindex','usemap','value', - - // SVG - 'wrap','clip','cx','cy','d','dy','dy','in','in2','k1','k2','k3','k4', - 'mask','mode','opacity','order','overflow','path','points','radius', - 'rx','ry','scale','stroke','stroke-width','transform','u1','u2','r','x', - 'y','x1','viewbox','x2','y1','y2','z','fill', - - // 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 ... should be returned */ - var WHOLE_DOCUMENT = false; - - /* Decide if a DOM node or a string should be returned */ - var RETURN_DOM = false; - - /* Output should be safe for jQuery's $() factory? */ - var SAFE_FOR_JQUERY = false; - - /* Ideally, do not touch anything below this line */ - /* ______________________________________________ */ - - - /** - * _parseConfig - * - * @param optional config literal - */ - var _parseConfig = function(cfg) { - cfg.ALLOWED_ATTR ? ALLOWED_ATTR = cfg.ALLOWED_ATTR : null; - cfg.ALLOWED_TAGS ? ALLOWED_TAGS = cfg.ALLOWED_TAGS : null; - cfg.ALLOW_DATA_ATTR ? ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR : null; - cfg.SAFE_FOR_JQUERY ? SAFE_FOR_JQUERY = cfg.SAFE_FOR_JQUERY : null; - cfg.WHOLE_DOCUMENT ? WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT : null; - cfg.RETURN_DOM ? RETURN_DOM = cfg.RETURN_DOM : null; - }; +;(function(root, factory) { + 'use strict'; + if (typeof define === "function" && define.amd) { + define(factory); + } else { + root.DOMPurify = factory(); + } +}(this, function() { + 'use strict'; + + var DOMPurify = {}; + DOMPurify.sanitize = function(dirty, cfg) { + + /******* /*************************************************************** + ****** / * Don't allow script elements. Or event handlers. * + ***** / * And careful with SVG's "values" attribute. * + **** / * Other than that, you can basically allow whatever you want. * + *** / * We'll make it safe for you. * + ** / ******************************************************************* + * / + /* allowed element names */ + var ALLOWED_TAGS = [ + + // 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','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', + + //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' + ]; + + /* Decide if custom data attributes are okay */ + var ALLOW_DATA_ATTR = true; + + /* Allowed attribute names */ + var ALLOWED_ATTR = [ + + // HTML + 'name', 'id','href','action','class','title','alt','src', 'type', + 'height','width', 'method','rev','rel','accept','align','autocomplete', + 'xmlns','bgcolor','border','checked','cite','color','cols','colspan', + 'coords','datetime','default','dir','disabled','download','enctype', + 'for','headers','hidden','high','hreflang','ismap','label','lang', + 'list','loop', 'low','max','maxlength','media','min','multiple', + 'novalidate','open','optimum','pattern','placeholder','poster', + 'preload','pubdate','radiogroup','readonly','required','reversed', + 'rows','rowspan','spellcheck','scope','selected','shape','size','span', + 'srclang','start','step','style','summary','tabindex','usemap','value', + + // SVG + 'wrap','clip','cx','cy','d','dy','dy','in','in2','k1','k2','k3','k4', + 'mask','mode','opacity','order','overflow','path','points','radius', + 'rx','ry','scale','stroke','stroke-width','transform','u1','u2','r','x', + 'y','x1','viewbox','x2','y1','y2','z','fill', + + // 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 ... should be returned */ + var WHOLE_DOCUMENT = false; + + /* Decide if a DOM node or a string should be returned */ + var RETURN_DOM = false; + + /* Output should be safe for jQuery's $() factory? */ + var SAFE_FOR_JQUERY = false; + + /* Ideally, do not touch anything below this line */ + /* ______________________________________________ */ + + + /** + * _parseConfig + * + * @param optional config literal + */ + var _parseConfig = function(cfg) { + cfg.ALLOWED_ATTR ? ALLOWED_ATTR = cfg.ALLOWED_ATTR : null; + cfg.ALLOWED_TAGS ? ALLOWED_TAGS = cfg.ALLOWED_TAGS : null; + cfg.ALLOW_DATA_ATTR ? ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR : null; + cfg.SAFE_FOR_JQUERY ? SAFE_FOR_JQUERY = cfg.SAFE_FOR_JQUERY : null; + cfg.WHOLE_DOCUMENT ? WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT : null; + cfg.RETURN_DOM ? RETURN_DOM = cfg.RETURN_DOM : null; + }; + + + /** + * _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, + 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.children && !(elm.children instanceof HTMLCollection)) + || typeof elm.nodeName !== 'string' + || typeof elm.textContent !== 'string' + || typeof elm.nodeType !== 'number' + || typeof elm.COMMENT_NODE !== 'number' + || typeof elm.setAttribute !== 'function' + || typeof elm.cloneNode !== 'function' + || typeof elm.removeAttributeNode !== 'function' + || typeof elm.attributes.item !== 'function' + ) { + return true; + } + return false; + }; + + + /** + * _sanitizeElements + * + * @protect removeChild + * @protect nodeType + * @protect nodeName + * @protect textContent + * @protect currentNode + * + * @param node to check for permission to exist + * @return true if node was killed, false if left alive + */ + var _sanitizeElements = function(currentNode) { + if ( + _isClobbered(currentNode) + || currentNode.nodeType === currentNode.COMMENT_NODE + || ALLOWED_TAGS.indexOf(currentNode.nodeName.toLowerCase()) === -1 + ) { + currentNode.parentNode.removeChild(currentNode); + return true; + } + if (SAFE_FOR_JQUERY && !currentNode.firstElementChild) { + currentNode.textContent = currentNode.textContent.replace(/= 0; attr--) { + var tmp = clonedNode.attributes[attr]; + currentNode.removeAttribute(currentNode.attributes[attr].name); + + if (tmp instanceof Attr) { + if ( + (ALLOWED_ATTR.indexOf(tmp.name.toLowerCase()) > -1 || + (ALLOW_DATA_ATTR && tmp.name.match(/^data-[\w-]+/i))) + && !tmp.value.replace(/[\x00-\x20]/g,'').match(regex) + ) { + currentNode.setAttribute(tmp.name, tmp.value); + } + } + } + }; + + + /** + * _sanitizeShadowDOM + * + * @param fragment to iterate over recursively + * @return void + */ + var _sanitizeShadowDOM = function(fragment) { + var shadowNode; + var shadowIterator = _createIterator(fragment); + + while (shadowNode = shadowIterator.nextNode()) { + /* Sanitize tags and elements */ + if (_sanitizeElements(shadowNode)) { + continue; + } - /** - * _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, - function() { return NodeFilter.FILTER_ACCEPT; }, - false - ); - }; + /* Deep shadow DOM detected */ + if (shadowNode.content instanceof DocumentFragment) { + _sanitizeShadowDOM(shadowNode.content); + } + /* Check attributes, sanitize if necessary */ + _sanitizeAttributes(shadowNode); + } + }; - /** - * _isClobbered - * - * @param element to check for clobbering attacks - * @return true if clobbered, false if safe - */ - var _isClobbered = function(elm) { - if ( - (elm.children && !(elm.children instanceof HTMLCollection)) - || typeof elm.nodeName !== 'string' - || typeof elm.textContent !== 'string' - || typeof elm.nodeType !== 'number' - || typeof elm.COMMENT_NODE !== 'number' - || typeof elm.setAttribute !== 'function' - || typeof elm.cloneNode !== 'function' - || typeof elm.removeAttributeNode !== 'function' - || typeof elm.attributes.item !== 'function' - ) { - return true; - } - return false; - }; + /* Assign config vars */ + cfg ? _parseConfig(cfg) : null; + + + /* Create documents to map markup to */ + var dom = document.implementation.createHTMLDocument(''); + dom.body.parentNode.removeChild(dom.body.parentNode.firstElementChild); + dom.body.outerHTML = dirty; + var body = WHOLE_DOCUMENT ? dom.body.parentNode : dom.body; - /** - * _sanitizeElements - * - * @protect removeChild - * @protect nodeType - * @protect nodeName - * @protect textContent - * @protect currentNode - * - * @param node to check for permission to exist - * @return true if node was killed, false if left alive - */ - var _sanitizeElements = function(currentNode) { if ( - _isClobbered(currentNode) - || currentNode.nodeType === currentNode.COMMENT_NODE - || ALLOWED_TAGS.indexOf(currentNode.nodeName.toLowerCase()) === -1 + !(dom.body instanceof HTMLBodyElement) || + !(dom.body instanceof HTMLHtmlElement) ) { - currentNode.parentNode.removeChild(currentNode); - return true; - } - if (SAFE_FOR_JQUERY && !currentNode.firstElementChild) { - currentNode.textContent = currentNode.textContent.replace(/= 0; attr--) { - var tmp = clonedNode.attributes[attr]; - currentNode.removeAttribute(currentNode.attributes[attr].name); - - if (tmp instanceof Attr) { - if ( - (ALLOWED_ATTR.indexOf(tmp.name.toLowerCase()) > -1 || - (ALLOW_DATA_ATTR && tmp.name.match(/^data-[\w-]+/i))) - && !tmp.value.replace(/[\x00-\x20]/g,'').match(regex) - ) { - currentNode.setAttribute(tmp.name, tmp.value); - } - } - } - }; + /* Get node iterator */ + var currentNode; + var nodeIterator = _createIterator(body); - /** - * _sanitizeShadowDOM - * - * @param fragment to iterate over recursively - * @return void - */ - var _sanitizeShadowDOM = function(fragment) { - var shadowNode; - var shadowIterator = _createIterator(fragment); - - while (shadowNode = shadowIterator.nextNode()) { + /* Now start iterating over the created document */ + while (currentNode = nodeIterator.nextNode()) { /* Sanitize tags and elements */ - if (_sanitizeElements(shadowNode)) { + if (_sanitizeElements(currentNode)) { continue; } - /* Deep shadow DOM detected */ - if (shadowNode.content instanceof DocumentFragment) { - _sanitizeShadowDOM(shadowNode.content); + /* Shadow DOM detected, sanitize it */ + if (currentNode.content instanceof DocumentFragment) { + _sanitizeShadowDOM(currentNode.content); } /* Check attributes, sanitize if necessary */ - _sanitizeAttributes(shadowNode); + _sanitizeAttributes(currentNode); } - }; - /* Assign config vars */ - cfg ? _parseConfig(cfg) : null; - - - /* Create documents to map markup to */ - var dom = document.implementation.createHTMLDocument(''); - dom.body.parentNode.removeChild(dom.body.parentNode.firstElementChild); - dom.body.outerHTML = dirty; - var body = WHOLE_DOCUMENT ? dom.body.parentNode : dom.body; - - if ( - !(dom.body instanceof HTMLBodyElement) || - !(dom.body instanceof HTMLHtmlElement) - ) { - var freshdom = document.implementation.createHTMLDocument(''); - body = WHOLE_DOCUMENT - ? freshdom.getElementsByTagName.call(dom,'html')[0] - : freshdom.getElementsByTagName.call(dom,'body')[0]; - } - - - /* Get node iterator */ - var currentNode; - var nodeIterator = _createIterator(body); - - - /* Now start iterating over the created document */ - while (currentNode = nodeIterator.nextNode()) { - /* Sanitize tags and elements */ - if (_sanitizeElements(currentNode)) { - continue; - } - - /* Shadow DOM detected, sanitize it */ - if (currentNode.content instanceof DocumentFragment) { - _sanitizeShadowDOM(currentNode.content); + /* Return sanitized string or DOM */ + if (RETURN_DOM) { + return body; } + return WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; + }; - /* Check attributes, sanitize if necessary */ - _sanitizeAttributes(currentNode); - } - + return DOMPurify; - /* Return sanitized string or DOM */ - if (RETURN_DOM) { - return body; - } - return WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; -}; -/* EOF */ +}));