sanitizer.spec.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { DefaultAllowlist, sanitizeHtml } from '../../../src/util/sanitizer.js'
  2. describe('Sanitizer', () => {
  3. describe('sanitizeHtml', () => {
  4. it('should return the same on empty string', () => {
  5. const empty = ''
  6. const result = sanitizeHtml(empty, DefaultAllowlist, null)
  7. expect(result).toEqual(empty)
  8. })
  9. it('should retain tags with valid URLs', () => {
  10. const validUrls = [
  11. '',
  12. 'http://abc',
  13. 'HTTP://abc',
  14. 'https://abc',
  15. 'HTTPS://abc',
  16. 'ftp://abc',
  17. 'FTP://abc',
  18. 'mailto:me@example.com',
  19. 'MAILTO:me@example.com',
  20. 'tel:123-123-1234',
  21. 'TEL:123-123-1234',
  22. 'sip:me@example.com',
  23. 'SIP:me@example.com',
  24. '#anchor',
  25. '/page1.md',
  26. 'http://JavaScript/my.js',
  27. 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', // Truncated.
  28. 'data:video/webm;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
  29. 'data:audio/opus;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
  30. 'unknown-scheme:abc'
  31. ]
  32. for (const url of validUrls) {
  33. const template = [
  34. '<div>',
  35. ` <a href="${url}">Click me</a>`,
  36. ' <span>Some content</span>',
  37. '</div>'
  38. ].join('')
  39. const result = sanitizeHtml(template, DefaultAllowlist, null)
  40. expect(result).toContain(`href="${url}"`)
  41. }
  42. })
  43. it('should sanitize template by removing tags with XSS', () => {
  44. const invalidUrls = [
  45. // eslint-disable-next-line no-script-url
  46. 'javascript:alert(7)',
  47. // eslint-disable-next-line no-script-url
  48. 'javascript:evil()',
  49. // eslint-disable-next-line no-script-url
  50. 'JavaScript:abc',
  51. ' javascript:abc',
  52. ' \n Java\n Script:abc',
  53. '&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;',
  54. '&#106&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;',
  55. '&#106 &#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;',
  56. '&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058',
  57. '&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A;',
  58. 'jav&#x09;ascript:alert();',
  59. 'jav\u0000ascript:alert();'
  60. ]
  61. for (const url of invalidUrls) {
  62. const template = [
  63. '<div>',
  64. ` <a href="${url}">Click me</a>`,
  65. ' <span>Some content</span>',
  66. '</div>'
  67. ].join('')
  68. const result = sanitizeHtml(template, DefaultAllowlist, null)
  69. expect(result).not.toContain(`href="${url}"`)
  70. }
  71. })
  72. it('should sanitize template and work with multiple regex', () => {
  73. const template = [
  74. '<div>',
  75. ' <a href="javascript:alert(7)" aria-label="This is a link" data-foo="bar">Click me</a>',
  76. ' <span>Some content</span>',
  77. '</div>'
  78. ].join('')
  79. const myDefaultAllowList = DefaultAllowlist
  80. // With the default allow list
  81. let result = sanitizeHtml(template, myDefaultAllowList, null)
  82. // `data-foo` won't be present
  83. expect(result).not.toContain('data-foo="bar"')
  84. // Add the following regex too
  85. myDefaultAllowList['*'].push(/^data-foo/)
  86. result = sanitizeHtml(template, myDefaultAllowList, null)
  87. expect(result).not.toContain('href="javascript:alert(7)') // This is in the default list
  88. expect(result).toContain('aria-label="This is a link"') // This is in the default list
  89. expect(result).toContain('data-foo="bar"') // We explicitly allow this
  90. })
  91. it('should allow aria attributes and safe attributes', () => {
  92. const template = [
  93. '<div aria-pressed="true">',
  94. ' <span class="test">Some content</span>',
  95. '</div>'
  96. ].join('')
  97. const result = sanitizeHtml(template, DefaultAllowlist, null)
  98. expect(result).toContain('aria-pressed')
  99. expect(result).toContain('class="test"')
  100. })
  101. it('should remove tags not in allowlist', () => {
  102. const template = [
  103. '<div>',
  104. ' <script>alert(7)</script>',
  105. '</div>'
  106. ].join('')
  107. const result = sanitizeHtml(template, DefaultAllowlist, null)
  108. expect(result).not.toContain('<script>')
  109. })
  110. it('should not use native api to sanitize if a custom function passed', () => {
  111. const template = [
  112. '<div>',
  113. ' <span>Some content</span>',
  114. '</div>'
  115. ].join('')
  116. function mySanitize(htmlUnsafe) {
  117. return htmlUnsafe
  118. }
  119. const spy = spyOn(DOMParser.prototype, 'parseFromString')
  120. const result = sanitizeHtml(template, DefaultAllowlist, mySanitize)
  121. expect(result).toEqual(template)
  122. expect(spy).not.toHaveBeenCalled()
  123. })
  124. it('should allow multiple sanitation passes of the same template', () => {
  125. const template = '<img src="test.jpg">'
  126. const firstResult = sanitizeHtml(template, DefaultAllowlist, null)
  127. const secondResult = sanitizeHtml(template, DefaultAllowlist, null)
  128. expect(firstResult).toContain('src')
  129. expect(secondResult).toContain('src')
  130. })
  131. })
  132. })