AttachmentViewer.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. 'use strict';
  2. const common = require('../common');
  3. const bodyTemplate = '<div id="attachmentPreview" class="col-md-12 text-center hidden"></div>' +
  4. '<div id="attachment" class="hidden"></div>' +
  5. '<div id="templates">' +
  6. '<div id="attachmenttemplate" role="alert" class="attachment hidden alert alert-info">' +
  7. '<span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span>' +
  8. '<a class="alert-link">Download attachment</a>' +
  9. '</div>' +
  10. '</div>';
  11. const createMockObjectURL = function(window, includeType = true) {
  12. if (typeof window.URL.createObjectURL === 'undefined') {
  13. Object.defineProperty(
  14. window.URL,
  15. 'createObjectURL',
  16. {value: function(blob) {
  17. return 'blob:' + (includeType ? blob.type : location.origin) + '/1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed';
  18. }}
  19. );
  20. }
  21. }
  22. describe('AttachmentViewer', function () {
  23. describe('setAttachment, showAttachment, removeAttachment, hideAttachment, hideAttachmentPreview, hasAttachment, getAttachment & moveAttachmentTo', function () {
  24. this.timeout(30000);
  25. jsc.property(
  26. 'displays & hides data as requested',
  27. common.jscMimeTypes(),
  28. 'string',
  29. 'string',
  30. 'string',
  31. 'string',
  32. // eslint-disable-next-line complexity
  33. function (mimeType, rawdata, filename, prefix, postfix) {
  34. let clean = jsdom(),
  35. data = 'data:' + mimeType + ';base64,' + common.btoa(rawdata),
  36. mimePrefix = mimeType.substring(0, 6),
  37. previewSupported = (
  38. mimePrefix === 'image/' ||
  39. mimePrefix === 'audio/' ||
  40. mimePrefix === 'video/' ||
  41. mimeType.match(/\/pdf/i)
  42. ),
  43. results = [],
  44. result = '';
  45. // text node of attachment will truncate at null byte
  46. if (filename === '\u0000') {
  47. filename = '';
  48. }
  49. prefix = prefix.replace(/%(s|d)/g, '%%');
  50. postfix = postfix.replace(/%(s|d)/g, '%%').replace(/<|>/g, '');
  51. $('body').html(bodyTemplate);
  52. createMockObjectURL(window, false);
  53. $.PrivateBin.AttachmentViewer.init();
  54. $.PrivateBin.Model.init();
  55. results.push(
  56. !$.PrivateBin.AttachmentViewer.hasAttachment() &&
  57. $('#attachment').hasClass('hidden') &&
  58. $('#attachment').children().length === 0 &&
  59. $('#attachmenttemplate').hasClass('hidden') &&
  60. $('#attachmentPreview').hasClass('hidden')
  61. );
  62. global.atob = common.atob;
  63. if (filename.length) {
  64. $.PrivateBin.AttachmentViewer.setAttachment(data, filename);
  65. } else {
  66. $.PrivateBin.AttachmentViewer.setAttachment(data);
  67. }
  68. // beyond this point we will get the blob URL instead of the data
  69. data = window.URL.createObjectURL(data);
  70. const attachment = $.PrivateBin.AttachmentViewer.getAttachments();
  71. results.push(
  72. $.PrivateBin.AttachmentViewer.hasAttachment() &&
  73. $('#attachment').hasClass('hidden') &&
  74. $('#attachment').children().length > 0 &&
  75. $('#attachmentPreview').hasClass('hidden') &&
  76. attachment[0][0] === data &&
  77. attachment[0][1] === filename
  78. );
  79. $.PrivateBin.AttachmentViewer.showAttachment();
  80. results.push(
  81. !$('#attachment').hasClass('hidden') &&
  82. $('#attachment').children().length > 0 &&
  83. (previewSupported ? !$('#attachmentPreview').hasClass('hidden') : $('#attachmentPreview').hasClass('hidden'))
  84. );
  85. $.PrivateBin.AttachmentViewer.hideAttachment();
  86. results.push(
  87. $('#attachment').hasClass('hidden') &&
  88. (previewSupported ? !$('#attachmentPreview').hasClass('hidden') : $('#attachmentPreview').hasClass('hidden'))
  89. );
  90. if (previewSupported) {
  91. $.PrivateBin.AttachmentViewer.hideAttachmentPreview();
  92. results.push($('#attachmentPreview').hasClass('hidden'));
  93. }
  94. $.PrivateBin.AttachmentViewer.showAttachment();
  95. results.push(
  96. !$('#attachment').hasClass('hidden') &&
  97. (previewSupported ? !$('#attachmentPreview').hasClass('hidden') : $('#attachmentPreview').hasClass('hidden'))
  98. );
  99. let element = $('<div>');
  100. $.PrivateBin.AttachmentViewer.moveAttachmentTo(element, attachment[0], prefix + '%s' + postfix);
  101. // messageIDs with links get a relaxed treatment
  102. if (prefix.indexOf('<a') === -1 && postfix.indexOf('<a') === -1) {
  103. result = $('<textarea>').text((prefix + filename + postfix)).text();
  104. } else {
  105. result = DOMPurify.sanitize(
  106. prefix + $.PrivateBin.Helper.htmlEntities(filename) + postfix, {
  107. ALLOWED_TAGS: ['a', 'i', 'span'],
  108. ALLOWED_ATTR: ['href', 'id']
  109. }
  110. );
  111. }
  112. if (filename.length) {
  113. results.push(
  114. element.find('a')[0].href === data &&
  115. element.find('a')[0].getAttribute('download') === filename &&
  116. element.find('a')[0].text === result
  117. );
  118. } else {
  119. results.push(element.find('a')[0].href === data);
  120. }
  121. $.PrivateBin.AttachmentViewer.removeAttachment();
  122. results.push(
  123. $('#attachment').hasClass('hidden') &&
  124. $('#attachment').children().length === 0 &&
  125. $('#attachmentPreview').hasClass('hidden')
  126. );
  127. clean();
  128. return results.every(element => element);
  129. }
  130. );
  131. it(
  132. 'sanitizes file names',
  133. function() {
  134. const clean = jsdom();
  135. $('body').html(bodyTemplate);
  136. createMockObjectURL(window);
  137. $.PrivateBin.AttachmentViewer.init();
  138. $.PrivateBin.Model.init();
  139. global.atob = common.atob;
  140. const maliciousFileNames = [
  141. '<script>alert("☹️");//<a',
  142. '"><meta http-equiv="refresh" content="0;url=http://example.com/">.txt'
  143. ];
  144. for (const filename of maliciousFileNames) {
  145. $.PrivateBin.AttachmentViewer.setAttachment('data:;base64,', filename);
  146. assert.ok(!$('body').html().includes(filename), 'does not allow file name ' + filename);
  147. $.PrivateBin.AttachmentViewer.removeAttachment();
  148. }
  149. clean();
  150. }
  151. );
  152. it(
  153. 'sanitizes MIME types in attachments',
  154. function() {
  155. const clean = jsdom();
  156. $('body').html(bodyTemplate);
  157. createMockObjectURL(window);
  158. $.PrivateBin.AttachmentViewer.init();
  159. $.PrivateBin.Model.init();
  160. global.atob = common.atob;
  161. const maliciousMimeTypes = [
  162. // PDF bypasses
  163. 'application/x-pdf', // legacy, we don't need to support this
  164. 'text/html /pdf', // trips up Firefox and Chromium
  165. 'text/html(/pdf', // Chromium, see: https://chromium.googlesource.com/chromium/src/+/refs/tags/152.0.7949.0/net/base/mime_util.cc#521
  166. // SVG bypass
  167. 'text/html svg',
  168. 'text/html(svg',
  169. // invalid bytes after string
  170. 'image/png\x01'
  171. ];
  172. for (const mimeType of maliciousMimeTypes) {
  173. assert.ok(!$.PrivateBin.AttachmentViewer.isSafeMimeType(mimeType), 'does not treat as safe MIME type: '+ mimeType);
  174. $.PrivateBin.AttachmentViewer.setAttachment('data:' + mimeType + ';base64,', 'example file name');
  175. assert.ok(!$('body').html().includes(mimeType), 'does not allow MIME type: ' + mimeType);
  176. assert.ok(!$('body').html().includes(mimeType.toLowerCase()), 'does not allow lower cased MIME type: ' + mimeType);
  177. assert.ok(!$('body').html().includes('<img'), 'does not allow image MIME type: ' + mimeType);
  178. $.PrivateBin.AttachmentViewer.removeAttachment();
  179. }
  180. clean();
  181. }
  182. );
  183. it(
  184. 'supports safe MIME types in attachments',
  185. function() {
  186. const clean = jsdom();
  187. $('body').html(bodyTemplate);
  188. createMockObjectURL(window);
  189. $.PrivateBin.AttachmentViewer.init();
  190. $.PrivateBin.Model.init();
  191. global.atob = common.atob;
  192. const supportedSafeMimeTypes = [
  193. 'text/plain',
  194. 'image/png',
  195. 'image/jpeg'
  196. ];
  197. for (const mimeType of supportedSafeMimeTypes) {
  198. assert.ok($.PrivateBin.AttachmentViewer.isSafeMimeType(mimeType), 'treats as safe MIME type: '+ mimeType);
  199. }
  200. clean();
  201. }
  202. );
  203. it(
  204. 'supports safe MIME type previews in attachments',
  205. function() {
  206. const clean = jsdom();
  207. $('body').html(bodyTemplate);
  208. createMockObjectURL(window);
  209. $.PrivateBin.AttachmentViewer.init();
  210. $.PrivateBin.Model.init();
  211. global.atob = common.atob;
  212. const supportedPreviewMimeTypes = [
  213. 'application/pdf',
  214. 'audio/wav',
  215. 'video/avi'
  216. ];
  217. for (const mimeType of supportedPreviewMimeTypes) {
  218. assert.ok($.PrivateBin.AttachmentViewer.isSafeMimeType(mimeType), 'treats as safe preview MIME type: '+ mimeType);
  219. $.PrivateBin.AttachmentViewer.setAttachment('data:' + mimeType + ';base64,', 'example file name');
  220. assert.ok($('body').html().includes(mimeType), 'allows MIME type: ' + mimeType);
  221. $.PrivateBin.AttachmentViewer.removeAttachment();
  222. }
  223. clean();
  224. }
  225. );
  226. it(
  227. 'special case sanitizes potentially unsafe SVG previews',
  228. function() {
  229. const clean = jsdom();
  230. $('body').html(bodyTemplate);
  231. createMockObjectURL(window);
  232. $.PrivateBin.AttachmentViewer.init();
  233. $.PrivateBin.Model.init();
  234. global.atob = common.atob;
  235. // special case: not a safe type, but renders a sanitized preview
  236. const svgMimeTypes = [
  237. 'image/svg+xml',
  238. 'image/SVG+xml',
  239. 'image/sVg'
  240. ];
  241. for (const mimeType of svgMimeTypes) {
  242. assert.ok(!$.PrivateBin.AttachmentViewer.isSafeMimeType(mimeType), 'treats as unsafe MIME type: '+ mimeType);
  243. $.PrivateBin.AttachmentViewer.setAttachment('data:' + mimeType + ';base64,', 'example file name');
  244. assert.ok($('body').html().includes('image/svg+xml'), 'allows sanitized MIME type: ' + mimeType);
  245. $.PrivateBin.AttachmentViewer.removeAttachment();
  246. }
  247. clean();
  248. }
  249. );
  250. });
  251. });