purify.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812
  1. ;(function(factory) {
  2. 'use strict';
  3. /* global window: false, define: false, module: false */
  4. var root = typeof window === 'undefined' ? null : window;
  5. if (typeof define === 'function' && define.amd) {
  6. define(function(){ return factory(root); });
  7. } else if (typeof module !== 'undefined') {
  8. module.exports = factory(root);
  9. } else {
  10. root.DOMPurify = factory(root);
  11. }
  12. }(function factory(window) {
  13. 'use strict';
  14. var DOMPurify = function(window) {
  15. return factory(window);
  16. };
  17. /**
  18. * Version label, exposed for easier checks
  19. * if DOMPurify is up to date or not
  20. */
  21. DOMPurify.version = '0.7.4';
  22. if (!window || !window.document || window.document.nodeType !== 9) {
  23. // not running in a browser, provide a factory function
  24. // so that you can pass your own Window
  25. DOMPurify.isSupported = false;
  26. return DOMPurify;
  27. }
  28. var document = window.document;
  29. var originalDocument = document;
  30. var DocumentFragment = window.DocumentFragment;
  31. var HTMLTemplateElement = window.HTMLTemplateElement;
  32. var NodeFilter = window.NodeFilter;
  33. var NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap;
  34. var Text = window.Text;
  35. var Comment = window.Comment;
  36. var DOMParser = window.DOMParser;
  37. // As per issue #47, the web-components registry is inherited by a
  38. // new document created via createHTMLDocument. As per the spec
  39. // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
  40. // a new empty registry is used when creating a template contents owner
  41. // document, so we use that as our parent document to ensure nothing
  42. // is inherited.
  43. if (typeof HTMLTemplateElement === 'function') {
  44. var template = document.createElement('template');
  45. if (template.content && template.content.ownerDocument) {
  46. document = template.content.ownerDocument;
  47. }
  48. }
  49. var implementation = document.implementation;
  50. var createNodeIterator = document.createNodeIterator;
  51. var getElementsByTagName = document.getElementsByTagName;
  52. var createDocumentFragment = document.createDocumentFragment;
  53. var importNode = originalDocument.importNode;
  54. var hooks = {};
  55. /**
  56. * Expose whether this browser supports running the full DOMPurify.
  57. */
  58. DOMPurify.isSupported =
  59. typeof implementation.createHTMLDocument !== 'undefined' &&
  60. document.documentMode !== 9;
  61. /* Add properties to a lookup table */
  62. var _addToSet = function(set, array) {
  63. var l = array.length;
  64. while (l--) {
  65. if (typeof array[l] === 'string') {
  66. array[l] = array[l].toLowerCase();
  67. }
  68. set[array[l]] = true;
  69. }
  70. return set;
  71. };
  72. /* Shallow clone an object */
  73. var _cloneObj = function(object) {
  74. var newObject = {};
  75. var property;
  76. for (property in object) {
  77. if (object.hasOwnProperty(property)) {
  78. newObject[property] = object[property];
  79. }
  80. }
  81. return newObject;
  82. };
  83. /**
  84. * We consider the elements and attributes below to be safe. Ideally
  85. * don't add any new ones but feel free to remove unwanted ones.
  86. */
  87. /* allowed element names */
  88. var ALLOWED_TAGS = null;
  89. var DEFAULT_ALLOWED_TAGS = _addToSet({}, [
  90. // HTML
  91. 'a','abbr','acronym','address','area','article','aside','audio','b',
  92. 'bdi','bdo','big','blink','blockquote','body','br','button','canvas',
  93. 'caption','center','cite','code','col','colgroup','content','data',
  94. 'datalist','dd','decorator','del','details','dfn','dir','div','dl','dt',
  95. 'element','em','fieldset','figcaption','figure','font','footer','form',
  96. 'h1','h2','h3','h4','h5','h6','head','header','hgroup','hr','html','i',
  97. 'img','input','ins','kbd','label','legend','li','main','map','mark',
  98. 'marquee','menu','menuitem','meter','nav','nobr','ol','optgroup',
  99. 'option','output','p','pre','progress','q','rp','rt','ruby','s','samp',
  100. 'section','select','shadow','small','source','spacer','span','strike',
  101. 'strong','style','sub','summary','sup','table','tbody','td','template',
  102. 'textarea','tfoot','th','thead','time','tr','track','tt','u','ul','var',
  103. 'video','wbr',
  104. // SVG
  105. 'svg','altglyph','altglyphdef','altglyphitem','animatecolor',
  106. 'animatemotion','animatetransform','circle','clippath','defs','desc',
  107. 'ellipse','filter','font','g','glyph','glyphref','hkern','image','line',
  108. 'lineargradient','marker','mask','metadata','mpath','path','pattern',
  109. 'polygon','polyline','radialgradient','rect','stop','switch','symbol',
  110. 'text','textpath','title','tref','tspan','view','vkern',
  111. // SVG Filters
  112. 'feBlend','feColorMatrix','feComponentTransfer','feComposite',
  113. 'feConvolveMatrix','feDiffuseLighting','feDisplacementMap',
  114. 'feFlood','feFuncA','feFuncB','feFuncG','feFuncR','feGaussianBlur',
  115. 'feMerge','feMergeNode','feMorphology','feOffset',
  116. 'feSpecularLighting','feTile','feTurbulence',
  117. //MathML
  118. 'math','menclose','merror','mfenced','mfrac','mglyph','mi','mlabeledtr',
  119. 'mmuliscripts','mn','mo','mover','mpadded','mphantom','mroot','mrow',
  120. 'ms','mpspace','msqrt','mystyle','msub','msup','msubsup','mtable','mtd',
  121. 'mtext','mtr','munder','munderover',
  122. //Text
  123. '#text'
  124. ]);
  125. /* Allowed attribute names */
  126. var ALLOWED_ATTR = null;
  127. var DEFAULT_ALLOWED_ATTR = _addToSet({}, [
  128. // HTML
  129. 'accept','action','align','alt','autocomplete','background','bgcolor',
  130. 'border','cellpadding','cellspacing','checked','cite','class','clear','color',
  131. 'cols','colspan','coords','datetime','default','dir','disabled',
  132. 'download','enctype','face','for','headers','height','hidden','high','href',
  133. 'hreflang','id','ismap','label','lang','list','loop', 'low','max',
  134. 'maxlength','media','method','min','multiple','name','noshade','novalidate',
  135. 'nowrap','open','optimum','pattern','placeholder','poster','preload','pubdate',
  136. 'radiogroup','readonly','rel','required','rev','reversed','rows',
  137. 'rowspan','spellcheck','scope','selected','shape','size','span',
  138. 'srclang','start','src','step','style','summary','tabindex','title',
  139. 'type','usemap','valign','value','width','xmlns',
  140. // SVG
  141. 'accent-height','accumulate','additivive','alignment-baseline',
  142. 'ascent','attributename','attributetype','azimuth','basefrequency',
  143. 'baseline-shift','begin','bias','by','clip','clip-path','clip-rule',
  144. 'color','color-interpolation','color-interpolation-filters','color-profile',
  145. 'color-rendering','cx','cy','d','dx','dy','diffuseconstant','direction',
  146. 'display','divisor','dur','edgemode','elevation','end','fill','fill-opacity',
  147. 'fill-rule','filter','flood-color','flood-opacity','font-family','font-size',
  148. 'font-size-adjust','font-stretch','font-style','font-variant','font-weight',
  149. 'fx', 'fy','g1','g2','glyph-name','glyphref','gradientunits','gradienttransform',
  150. 'image-rendering','in','in2','k','k1','k2','k3','k4','kerning','keypoints',
  151. 'keysplines','keytimes','lengthadjust','letter-spacing','kernelmatrix',
  152. 'kernelunitlength','lighting-color','local','marker-end','marker-mid',
  153. 'marker-start','markerheight','markerunits','markerwidth','maskcontentunits',
  154. 'maskunits','max','mask','mode','min','numoctaves','offset','operator',
  155. 'opacity','order','orient','orientation','origin','overflow','paint-order',
  156. 'path','pathlength','patterncontentunits','patterntransform','patternunits',
  157. 'points','preservealpha','r','rx','ry','radius','refx','refy','repeatcount',
  158. 'repeatdur','restart','result','rotate','scale','seed','shape-rendering',
  159. 'specularconstant','specularexponent','spreadmethod','stddeviation','stitchtiles',
  160. 'stop-color','stop-opacity','stroke-dasharray','stroke-dashoffset','stroke-linecap',
  161. 'stroke-linejoin','stroke-miterlimit','stroke-opacity','stroke','stroke-width',
  162. 'surfacescale','targetx','targety','transform','text-anchor','text-decoration',
  163. 'text-rendering','textlength','u1','u2','unicode','values','viewbox',
  164. 'visibility','vert-adv-y','vert-origin-x','vert-origin-y','word-spacing',
  165. 'wrap','writing-mode','xchannelselector','ychannelselector','x','x1','x2',
  166. 'y','y1','y2','z','zoomandpan',
  167. // MathML
  168. 'accent','accentunder','bevelled','close','columnsalign','columnlines',
  169. 'columnspan','denomalign','depth','display','displaystyle','fence',
  170. 'frame','largeop','length','linethickness','lspace','lquote',
  171. 'mathbackground','mathcolor','mathsize','mathvariant','maxsize',
  172. 'minsize','movablelimits','notation','numalign','open','rowalign',
  173. 'rowlines','rowspacing','rowspan','rspace','rquote','scriptlevel',
  174. 'scriptminsize','scriptsizemultiplier','selection','separator',
  175. 'separators','stretchy','subscriptshift','supscriptshift','symmetric',
  176. 'voffset',
  177. // XML
  178. 'xlink:href','xml:id','xlink:title','xml:space','xmlns:xlink'
  179. ]);
  180. /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */
  181. var FORBID_TAGS = null;
  182. /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */
  183. var FORBID_ATTR = null;
  184. /* Decide if custom data attributes are okay */
  185. var ALLOW_DATA_ATTR = true;
  186. /* Decide if unknown protocols are okay */
  187. var ALLOW_UNKNOWN_PROTOCOLS = false;
  188. /* Output should be safe for jQuery's $() factory? */
  189. var SAFE_FOR_JQUERY = false;
  190. /* Output should be safe for common template engines.
  191. * This means, DOMPurify removes data attributes, mustaches and ERB
  192. */
  193. var SAFE_FOR_TEMPLATES = false;
  194. /* Specify template detection regex for SAFE_FOR_TEMPLATES mode */
  195. var MUSTACHE_EXPR = /\{\{[\s\S]*|[\s\S]*\}\}/gm;
  196. var ERB_EXPR = /<%[\s\S]*|[\s\S]*%>/gm;
  197. /* Decide if document with <html>... should be returned */
  198. var WHOLE_DOCUMENT = false;
  199. /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html string.
  200. * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead
  201. */
  202. var RETURN_DOM = false;
  203. /* Decide if a DOM `DocumentFragment` should be returned, instead of a html string */
  204. var RETURN_DOM_FRAGMENT = false;
  205. /* If `RETURN_DOM` or `RETURN_DOM_FRAGMENT` is enabled, decide if the returned DOM
  206. * `Node` is imported into the current `Document`. If this flag is not enabled the
  207. * `Node` will belong (its ownerDocument) to a fresh `HTMLDocument`, created by
  208. * DOMPurify. */
  209. var RETURN_DOM_IMPORT = false;
  210. /* Output should be free from DOM clobbering attacks? */
  211. var SANITIZE_DOM = true;
  212. /* Keep element content when removing element? */
  213. var KEEP_CONTENT = true;
  214. /* Tags to ignore content of when KEEP_CONTENT is true */
  215. var FORBID_CONTENTS = _addToSet({}, [
  216. 'audio', 'head', 'math', 'script', 'style', 'svg', 'video'
  217. ]);
  218. /* Tags that are safe for data: URIs */
  219. var DATA_URI_TAGS = _addToSet({}, [
  220. 'audio', 'video', 'img', 'source'
  221. ]);
  222. /* Attributes safe for values like "javascript:" */
  223. var URI_SAFE_ATTRIBUTES = _addToSet({}, [
  224. 'alt','class','for','id','label','name','pattern','placeholder',
  225. 'summary','title','value','style','xmlns'
  226. ]);
  227. /* Keep a reference to config to pass to hooks */
  228. var CONFIG = null;
  229. /* Ideally, do not touch anything below this line */
  230. /* ______________________________________________ */
  231. var formElement = document.createElement('form');
  232. /**
  233. * _parseConfig
  234. *
  235. * @param optional config literal
  236. */
  237. var _parseConfig = function(cfg) {
  238. /* Shield configuration object from tampering */
  239. if (typeof cfg !== 'object') {
  240. cfg = {};
  241. }
  242. /* Set configuration parameters */
  243. ALLOWED_TAGS = 'ALLOWED_TAGS' in cfg ?
  244. _addToSet({}, cfg.ALLOWED_TAGS) : DEFAULT_ALLOWED_TAGS;
  245. ALLOWED_ATTR = 'ALLOWED_ATTR' in cfg ?
  246. _addToSet({}, cfg.ALLOWED_ATTR) : DEFAULT_ALLOWED_ATTR;
  247. FORBID_TAGS = 'FORBID_TAGS' in cfg ?
  248. _addToSet({}, cfg.FORBID_TAGS) : {};
  249. FORBID_ATTR = 'FORBID_ATTR' in cfg ?
  250. _addToSet({}, cfg.FORBID_ATTR) : {};
  251. ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
  252. ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
  253. SAFE_FOR_JQUERY = cfg.SAFE_FOR_JQUERY || false; // Default false
  254. SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false
  255. WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false
  256. RETURN_DOM = cfg.RETURN_DOM || false; // Default false
  257. RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false
  258. RETURN_DOM_IMPORT = cfg.RETURN_DOM_IMPORT || false; // Default false
  259. SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true
  260. KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
  261. if (SAFE_FOR_TEMPLATES) {
  262. ALLOW_DATA_ATTR = false;
  263. }
  264. if (RETURN_DOM_FRAGMENT) {
  265. RETURN_DOM = true;
  266. }
  267. /* Merge configuration parameters */
  268. if (cfg.ADD_TAGS) {
  269. if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
  270. ALLOWED_TAGS = _cloneObj(ALLOWED_TAGS);
  271. }
  272. _addToSet(ALLOWED_TAGS, cfg.ADD_TAGS);
  273. }
  274. if (cfg.ADD_ATTR) {
  275. if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
  276. ALLOWED_ATTR = _cloneObj(ALLOWED_ATTR);
  277. }
  278. _addToSet(ALLOWED_ATTR, cfg.ADD_ATTR);
  279. }
  280. /* Add #text in case KEEP_CONTENT is set to true */
  281. if (KEEP_CONTENT) { ALLOWED_TAGS['#text'] = true; }
  282. // Prevent further manipulation of configuration.
  283. // Not available in IE8, Safari 5, etc.
  284. if (Object && 'freeze' in Object) { Object.freeze(cfg); }
  285. CONFIG = cfg;
  286. };
  287. /**
  288. * _forceRemove
  289. *
  290. * @param a DOM node
  291. */
  292. var _forceRemove = function(node) {
  293. try {
  294. node.parentNode.removeChild(node);
  295. } catch (e) {
  296. node.outerHTML = '';
  297. }
  298. };
  299. /**
  300. * _initDocument
  301. *
  302. * @param a string of dirty markup
  303. * @return a DOM, filled with the dirty markup
  304. */
  305. var _initDocument = function(dirty) {
  306. /* Create a HTML document using DOMParser */
  307. var doc, body;
  308. try {
  309. doc = new DOMParser().parseFromString(dirty, 'text/html');
  310. } catch (e) {}
  311. /* Some browsers throw, some browsers return null for the code above
  312. DOMParser with text/html support is only in very recent browsers. */
  313. if (!doc) {
  314. doc = implementation.createHTMLDocument('');
  315. body = doc.body;
  316. body.parentNode.removeChild(body.parentNode.firstElementChild);
  317. body.outerHTML = dirty;
  318. }
  319. /* Work on whole document or just its body */
  320. if (typeof doc.getElementsByTagName === 'function') {
  321. return doc.getElementsByTagName(
  322. WHOLE_DOCUMENT ? 'html' : 'body')[0];
  323. }
  324. return getElementsByTagName.call(doc,
  325. WHOLE_DOCUMENT ? 'html' : 'body')[0];
  326. };
  327. /**
  328. * _createIterator
  329. *
  330. * @param document/fragment to create iterator for
  331. * @return iterator instance
  332. */
  333. var _createIterator = function(root) {
  334. return createNodeIterator.call(root.ownerDocument || root,
  335. root,
  336. NodeFilter.SHOW_ELEMENT
  337. | NodeFilter.SHOW_COMMENT
  338. | NodeFilter.SHOW_TEXT,
  339. function() { return NodeFilter.FILTER_ACCEPT; },
  340. false
  341. );
  342. };
  343. /**
  344. * _isClobbered
  345. *
  346. * @param element to check for clobbering attacks
  347. * @return true if clobbered, false if safe
  348. */
  349. var _isClobbered = function(elm) {
  350. if (elm instanceof Text || elm instanceof Comment) {
  351. return false;
  352. }
  353. if ( typeof elm.nodeName !== 'string'
  354. || typeof elm.textContent !== 'string'
  355. || typeof elm.removeChild !== 'function'
  356. || !(elm.attributes instanceof NamedNodeMap)
  357. || typeof elm.removeAttribute !== 'function'
  358. || typeof elm.setAttribute !== 'function'
  359. ) {
  360. return true;
  361. }
  362. return false;
  363. };
  364. /**
  365. * _sanitizeElements
  366. *
  367. * @protect nodeName
  368. * @protect textContent
  369. * @protect removeChild
  370. *
  371. * @param node to check for permission to exist
  372. * @return true if node was killed, false if left alive
  373. */
  374. var _sanitizeElements = function(currentNode) {
  375. var tagName, content;
  376. /* Execute a hook if present */
  377. _executeHook('beforeSanitizeElements', currentNode, null);
  378. /* Check if element is clobbered or can clobber */
  379. if (_isClobbered(currentNode)) {
  380. _forceRemove(currentNode);
  381. return true;
  382. }
  383. /* Now let's check the element's type and name */
  384. tagName = currentNode.nodeName.toLowerCase();
  385. /* Execute a hook if present */
  386. _executeHook('uponSanitizeElement', currentNode, {
  387. tagName: tagName
  388. });
  389. /* Remove element if anything forbids its presence */
  390. if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
  391. /* Keep content except for black-listed elements */
  392. if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]
  393. && typeof currentNode.insertAdjacentHTML === 'function') {
  394. try {
  395. currentNode.insertAdjacentHTML('AfterEnd', currentNode.innerHTML);
  396. } catch (e) {}
  397. }
  398. _forceRemove(currentNode);
  399. return true;
  400. }
  401. /* Convert markup to cover jQuery behavior */
  402. if (SAFE_FOR_JQUERY && !currentNode.firstElementChild &&
  403. (!currentNode.content || !currentNode.content.firstElementChild)) {
  404. currentNode.innerHTML = currentNode.textContent.replace(/</g, '&lt;');
  405. }
  406. /* Sanitize element content to be template-safe */
  407. if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) {
  408. /* Get the element's text content */
  409. content = currentNode.textContent;
  410. content = content.replace(MUSTACHE_EXPR, ' ');
  411. content = content.replace(ERB_EXPR, ' ');
  412. currentNode.textContent = content;
  413. }
  414. /* Execute a hook if present */
  415. _executeHook('afterSanitizeElements', currentNode, null);
  416. return false;
  417. };
  418. var DATA_ATTR = /^data-[\w.\u00B7-\uFFFF-]/;
  419. var IS_ALLOWED_URI = /^(?:(?:(?:f|ht)tps?|mailto|tel):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
  420. var IS_SCRIPT_OR_DATA = /^(?:\w+script|data):/i;
  421. /* This needs to be extensive thanks to Webkit/Blink's behavior */
  422. var ATTR_WHITESPACE = /[\x00-\x20\xA0\u1680\u180E\u2000-\u2029\u205f\u3000]/g;
  423. /**
  424. * _sanitizeAttributes
  425. *
  426. * @protect attributes
  427. * @protect nodeName
  428. * @protect removeAttribute
  429. * @protect setAttribute
  430. *
  431. * @param node to sanitize
  432. * @return void
  433. */
  434. var _sanitizeAttributes = function(currentNode) {
  435. var attr, name, value, lcName, idAttr, attributes, hookEvent, l;
  436. /* Execute a hook if present */
  437. _executeHook('beforeSanitizeAttributes', currentNode, null);
  438. attributes = currentNode.attributes;
  439. /* Check if we have attributes; if not we might have a text node */
  440. if (!attributes) { return; }
  441. hookEvent = {
  442. attrName: '',
  443. attrValue: '',
  444. keepAttr: true
  445. };
  446. l = attributes.length;
  447. /* Go backwards over all attributes; safely remove bad ones */
  448. while (l--) {
  449. attr = attributes[l];
  450. name = attr.name;
  451. value = attr.value;
  452. lcName = name.toLowerCase();
  453. /* Execute a hook if present */
  454. hookEvent.attrName = lcName;
  455. hookEvent.attrValue = value;
  456. hookEvent.keepAttr = true;
  457. _executeHook('uponSanitizeAttribute', currentNode, hookEvent );
  458. value = hookEvent.attrValue;
  459. /* Remove attribute */
  460. // Safari (iOS + Mac), last tested v8.0.5, crashes if you try to
  461. // remove a "name" attribute from an <img> tag that has an "id"
  462. // attribute at the time.
  463. if (lcName === 'name' &&
  464. currentNode.nodeName === 'IMG' && attributes.id) {
  465. idAttr = attributes.id;
  466. attributes = Array.prototype.slice.apply(attributes);
  467. currentNode.removeAttribute('id');
  468. currentNode.removeAttribute(name);
  469. if (attributes.indexOf(idAttr) > l) {
  470. currentNode.setAttribute('id', idAttr.value);
  471. }
  472. } else {
  473. // This avoids a crash in Safari v9.0 with double-ids.
  474. // The trick is to first set the id to be empty and then to
  475. // remove the attriubute
  476. if (name === 'id') {
  477. currentNode.setAttribute(name, '');
  478. }
  479. currentNode.removeAttribute(name);
  480. }
  481. /* Did the hooks approve of the attribute? */
  482. if (!hookEvent.keepAttr) {
  483. continue;
  484. }
  485. /* Make sure attribute cannot clobber */
  486. if (SANITIZE_DOM &&
  487. (lcName === 'id' || lcName === 'name') &&
  488. (value in window || value in document || value in formElement)) {
  489. continue;
  490. }
  491. /* Sanitize attribute content to be template-safe */
  492. if (SAFE_FOR_TEMPLATES) {
  493. value = value.replace(MUSTACHE_EXPR, ' ');
  494. value = value.replace(ERB_EXPR, ' ');
  495. }
  496. if (
  497. /* Check the name is permitted */
  498. (ALLOWED_ATTR[lcName] && !FORBID_ATTR[lcName] && (
  499. /* Check no script, data or unknown possibly unsafe URI
  500. unless we know URI values are safe for that attribute */
  501. URI_SAFE_ATTRIBUTES[lcName] ||
  502. IS_ALLOWED_URI.test(value.replace(ATTR_WHITESPACE,'')) ||
  503. /* Keep image data URIs alive if src is allowed */
  504. (lcName === 'src' && value.indexOf('data:') === 0 &&
  505. DATA_URI_TAGS[currentNode.nodeName.toLowerCase()])
  506. )) ||
  507. /* Allow potentially valid data-* attributes:
  508. * At least one character after "-" (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
  509. * XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
  510. * We don't need to check the value; it's always URI safe.
  511. */
  512. (ALLOW_DATA_ATTR && DATA_ATTR.test(lcName)) ||
  513. /* Allow unknown protocols:
  514. * This provides support for links that are handled by protocol handlers which may be unknown
  515. * ahead of time, e.g. fb:, spotify:
  516. */
  517. (ALLOW_UNKNOWN_PROTOCOLS && !IS_SCRIPT_OR_DATA.test(value.replace(ATTR_WHITESPACE,'')))
  518. ) {
  519. /* Handle invalid data-* attribute set by try-catching it */
  520. try {
  521. currentNode.setAttribute(name, value);
  522. } catch (e) {}
  523. }
  524. }
  525. /* Execute a hook if present */
  526. _executeHook('afterSanitizeAttributes', currentNode, null);
  527. };
  528. /**
  529. * _sanitizeShadowDOM
  530. *
  531. * @param fragment to iterate over recursively
  532. * @return void
  533. */
  534. var _sanitizeShadowDOM = function(fragment) {
  535. var shadowNode;
  536. var shadowIterator = _createIterator(fragment);
  537. /* Execute a hook if present */
  538. _executeHook('beforeSanitizeShadowDOM', fragment, null);
  539. while ( (shadowNode = shadowIterator.nextNode()) ) {
  540. /* Execute a hook if present */
  541. _executeHook('uponSanitizeShadowNode', shadowNode, null);
  542. /* Sanitize tags and elements */
  543. if (_sanitizeElements(shadowNode)) {
  544. continue;
  545. }
  546. /* Deep shadow DOM detected */
  547. if (shadowNode.content instanceof DocumentFragment) {
  548. _sanitizeShadowDOM(shadowNode.content);
  549. }
  550. /* Check attributes, sanitize if necessary */
  551. _sanitizeAttributes(shadowNode);
  552. }
  553. /* Execute a hook if present */
  554. _executeHook('afterSanitizeShadowDOM', fragment, null);
  555. };
  556. /**
  557. * _executeHook
  558. * Execute user configurable hooks
  559. *
  560. * @param {String} entryPoint Name of the hook's entry point
  561. * @param {Node} currentNode
  562. */
  563. var _executeHook = function(entryPoint, currentNode, data) {
  564. if (!hooks[entryPoint]) { return; }
  565. hooks[entryPoint].forEach(function(hook) {
  566. hook.call(DOMPurify, currentNode, data, CONFIG);
  567. });
  568. };
  569. /**
  570. * sanitize
  571. * Public method providing core sanitation functionality
  572. *
  573. * @param {String} dirty string
  574. * @param {Object} configuration object
  575. */
  576. DOMPurify.sanitize = function(dirty, cfg) {
  577. var body, currentNode, oldNode, nodeIterator, returnNode;
  578. /* Make sure we have a string to sanitize.
  579. DO NOT return early, as this will return the wrong type if
  580. the user has requested a DOM object rather than a string */
  581. if (!dirty) {
  582. dirty = '';
  583. }
  584. /* Stringify, in case dirty is an object */
  585. if (typeof dirty !== 'string') {
  586. if (typeof dirty.toString !== 'function') {
  587. throw new TypeError('toString is not a function');
  588. } else {
  589. dirty = dirty.toString();
  590. }
  591. }
  592. /* Check we can run. Otherwise fall back or ignore */
  593. if (!DOMPurify.isSupported) {
  594. if (typeof window.toStaticHTML === 'object'
  595. || typeof window.toStaticHTML === 'function') {
  596. return window.toStaticHTML(dirty);
  597. }
  598. return dirty;
  599. }
  600. /* Assign config vars */
  601. _parseConfig(cfg);
  602. /* Exit directly if we have nothing to do */
  603. if (!RETURN_DOM && !WHOLE_DOCUMENT && dirty.indexOf('<') === -1) {
  604. return dirty;
  605. }
  606. /* Initialize the document to work on */
  607. body = _initDocument(dirty);
  608. /* Check we have a DOM node from the data */
  609. if (!body) {
  610. return RETURN_DOM ? null : '';
  611. }
  612. /* Get node iterator */
  613. nodeIterator = _createIterator(body);
  614. /* Now start iterating over the created document */
  615. while ( (currentNode = nodeIterator.nextNode()) ) {
  616. /* Fix IE's strange behavior with manipulated textNodes #89 */
  617. if (currentNode.nodeType === 3 && currentNode === oldNode) {
  618. continue;
  619. }
  620. /* Sanitize tags and elements */
  621. if (_sanitizeElements(currentNode)) {
  622. continue;
  623. }
  624. /* Shadow DOM detected, sanitize it */
  625. if (currentNode.content instanceof DocumentFragment) {
  626. _sanitizeShadowDOM(currentNode.content);
  627. }
  628. /* Check attributes, sanitize if necessary */
  629. _sanitizeAttributes(currentNode);
  630. oldNode = currentNode;
  631. }
  632. /* Return sanitized string or DOM */
  633. if (RETURN_DOM) {
  634. if (RETURN_DOM_FRAGMENT) {
  635. returnNode = createDocumentFragment.call(body.ownerDocument);
  636. while (body.firstChild) {
  637. returnNode.appendChild(body.firstChild);
  638. }
  639. } else {
  640. returnNode = body;
  641. }
  642. if (RETURN_DOM_IMPORT) {
  643. /* adoptNode() is not used because internal state is not reset
  644. (e.g. the past names map of a HTMLFormElement), this is safe
  645. in theory but we would rather not risk another attack vector.
  646. The state that is cloned by importNode() is explicitly defined
  647. by the specs. */
  648. returnNode = importNode.call(originalDocument, returnNode, true);
  649. }
  650. return returnNode;
  651. }
  652. return WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;
  653. };
  654. /**
  655. * addHook
  656. * Public method to add DOMPurify hooks
  657. *
  658. * @param {String} entryPoint
  659. * @param {Function} hookFunction
  660. */
  661. DOMPurify.addHook = function(entryPoint, hookFunction) {
  662. if (typeof hookFunction !== 'function') { return; }
  663. hooks[entryPoint] = hooks[entryPoint] || [];
  664. hooks[entryPoint].push(hookFunction);
  665. };
  666. /**
  667. * removeHook
  668. * Public method to remove a DOMPurify hook at a given entryPoint
  669. * (pops it from the stack of hooks if more are present)
  670. *
  671. * @param {String} entryPoint
  672. * @return void
  673. */
  674. DOMPurify.removeHook = function(entryPoint) {
  675. if (hooks[entryPoint]) {
  676. hooks[entryPoint].pop();
  677. }
  678. };
  679. /**
  680. * removeHooks
  681. * Public method to remove all DOMPurify hooks at a given entryPoint
  682. *
  683. * @param {String} entryPoint
  684. * @return void
  685. */
  686. DOMPurify.removeHooks = function(entryPoint) {
  687. if (hooks[entryPoint]) {
  688. hooks[entryPoint] = [];
  689. }
  690. };
  691. /**
  692. * removeAllHooks
  693. * Public method to remove all DOMPurify hooks
  694. *
  695. * @return void
  696. */
  697. DOMPurify.removeAllHooks = function() {
  698. hooks = [];
  699. };
  700. return DOMPurify;
  701. }));