secureDOMPurify.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import DOMPurify from 'dompurify';
  2. // Centralized secure DOMPurify configuration to prevent XSS and CSS injection attacks
  3. export function getSecureDOMPurifyConfig() {
  4. return {
  5. // Block dangerous elements that can cause XSS and CSS injection
  6. FORBID_TAGS: [
  7. 'svg', 'defs', 'use', 'g', 'symbol', 'marker', 'pattern', 'mask', 'clipPath',
  8. 'linearGradient', 'radialGradient', 'stop', 'animate', 'animateTransform',
  9. 'animateMotion', 'set', 'switch', 'foreignObject', 'script', 'style', 'link',
  10. 'meta', 'iframe', 'object', 'embed', 'applet', 'form', 'input', 'textarea',
  11. 'select', 'option', 'button', 'label', 'fieldset', 'legend', 'frameset',
  12. 'frame', 'noframes', 'base', 'basefont', 'isindex', 'dir', 'menu', 'menuitem'
  13. ],
  14. // Block dangerous attributes that can cause XSS and CSS injection
  15. FORBID_ATTR: [
  16. 'xlink:href', 'href', 'onload', 'onerror', 'onclick', 'onmouseover',
  17. 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect',
  18. 'onunload', 'onresize', 'onscroll', 'onkeydown', 'onkeyup', 'onkeypress',
  19. 'onmousedown', 'onmouseup', 'onmouseover', 'onmouseout', 'onmousemove',
  20. 'ondblclick', 'oncontextmenu', 'onwheel', 'ontouchstart', 'ontouchend',
  21. 'ontouchmove', 'ontouchcancel', 'onabort', 'oncanplay', 'oncanplaythrough',
  22. 'ondurationchange', 'onemptied', 'onended', 'onerror', 'onloadeddata',
  23. 'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying',
  24. 'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled',
  25. 'onsuspend', 'ontimeupdate', 'onvolumechange', 'onwaiting', 'onbeforeunload',
  26. 'onhashchange', 'onpagehide', 'onpageshow', 'onpopstate', 'onstorage',
  27. 'onunload', 'style', 'class', 'id', 'data-*', 'aria-*'
  28. ],
  29. // Allow only safe image formats and protocols
  30. ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
  31. // Remove dangerous protocols
  32. ALLOW_UNKNOWN_PROTOCOLS: false,
  33. // Sanitize URLs to prevent malicious content loading
  34. SANITIZE_DOM: true,
  35. // Remove dangerous elements completely
  36. KEEP_CONTENT: false,
  37. // Additional security measures
  38. ADD_ATTR: [],
  39. // Block data URIs that could contain malicious content
  40. ALLOW_DATA_ATTR: false,
  41. // Custom hook to further sanitize content
  42. HOOKS: {
  43. uponSanitizeElement: function(node, data) {
  44. // Block any remaining dangerous elements
  45. const dangerousTags = ['svg', 'style', 'script', 'link', 'meta', 'iframe', 'object', 'embed', 'applet'];
  46. if (node.tagName && dangerousTags.includes(node.tagName.toLowerCase())) {
  47. if (process.env.DEBUG === 'true') {
  48. console.warn('Blocked potentially dangerous element:', node.tagName);
  49. }
  50. return false;
  51. }
  52. // Block img tags with SVG data URIs that could contain malicious JavaScript
  53. if (node.tagName && node.tagName.toLowerCase() === 'img') {
  54. const src = node.getAttribute('src');
  55. if (src) {
  56. // Block all SVG data URIs to prevent XSS via embedded JavaScript
  57. if (src.startsWith('data:image/svg') || src.endsWith('.svg')) {
  58. if (process.env.DEBUG === 'true') {
  59. console.warn('Blocked potentially malicious SVG image:', src);
  60. }
  61. return false;
  62. }
  63. // Additional check for base64 encoded SVG with script tags
  64. if (src.startsWith('data:image/svg+xml;base64,')) {
  65. try {
  66. const base64Content = src.split(',')[1];
  67. const decodedContent = atob(base64Content);
  68. if (decodedContent.includes('<script') || decodedContent.includes('javascript:')) {
  69. if (process.env.DEBUG === 'true') {
  70. console.warn('Blocked SVG with embedded JavaScript:', src.substring(0, 100) + '...');
  71. }
  72. return false;
  73. }
  74. } catch (e) {
  75. // If decoding fails, block it as a safety measure
  76. if (process.env.DEBUG === 'true') {
  77. console.warn('Blocked malformed SVG data URI:', src);
  78. }
  79. return false;
  80. }
  81. }
  82. }
  83. }
  84. // Block elements with dangerous attributes
  85. const dangerousAttrs = ['style', 'onload', 'onerror', 'onclick', 'onmouseover', 'onfocus', 'onblur'];
  86. for (const attr of dangerousAttrs) {
  87. if (node.hasAttribute && node.hasAttribute(attr)) {
  88. if (process.env.DEBUG === 'true') {
  89. console.warn('Blocked element with dangerous attribute:', node.tagName, attr);
  90. }
  91. return false;
  92. }
  93. }
  94. return true;
  95. },
  96. uponSanitizeAttribute: function(node, data) {
  97. // Block style attributes completely
  98. if (data.attrName === 'style') {
  99. if (process.env.DEBUG === 'true') {
  100. console.warn('Blocked style attribute');
  101. }
  102. return false;
  103. }
  104. // Block class and id attributes that might be used for CSS injection
  105. if (data.attrName === 'class' || data.attrName === 'id') {
  106. if (process.env.DEBUG === 'true') {
  107. console.warn('Blocked class/id attribute:', data.attrName, data.attrValue);
  108. }
  109. return false;
  110. }
  111. // Block data attributes
  112. if (data.attrName && data.attrName.startsWith('data-')) {
  113. if (process.env.DEBUG === 'true') {
  114. console.warn('Blocked data attribute:', data.attrName);
  115. }
  116. return false;
  117. }
  118. return true;
  119. }
  120. }
  121. };
  122. }
  123. // Convenience function for secure sanitization
  124. export function sanitizeHTML(html) {
  125. return DOMPurify.sanitize(html, getSecureDOMPurifyConfig());
  126. }
  127. // Convenience function for sanitizing text (no HTML)
  128. export function sanitizeText(text) {
  129. return DOMPurify.sanitize(text, {
  130. ALLOWED_TAGS: [],
  131. ALLOWED_ATTR: [],
  132. KEEP_CONTENT: true
  133. });
  134. }