|
|
@@ -1060,13 +1060,9 @@ jQuery.PrivateBin = (function($) {
|
|
|
*/
|
|
|
function arraybufferToString(messageArray)
|
|
|
{
|
|
|
- const array = new Uint8Array(messageArray);
|
|
|
- let message = '',
|
|
|
- i = 0;
|
|
|
- while(i < array.length) {
|
|
|
- message += String.fromCharCode(array[i++]);
|
|
|
- }
|
|
|
- return message;
|
|
|
+ return Array.from(new Uint8Array(messageArray))
|
|
|
+ .map(byte => String.fromCharCode(byte))
|
|
|
+ .join('');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -1082,11 +1078,10 @@ jQuery.PrivateBin = (function($) {
|
|
|
*/
|
|
|
function stringToArraybuffer(message)
|
|
|
{
|
|
|
- const messageArray = new Uint8Array(message.length);
|
|
|
- for (let i = 0; i < message.length; ++i) {
|
|
|
- messageArray[i] = message.charCodeAt(i);
|
|
|
- }
|
|
|
- return messageArray;
|
|
|
+ return Uint8Array.from(
|
|
|
+ message,
|
|
|
+ character => character.charCodeAt(0)
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -2959,10 +2954,10 @@ jQuery.PrivateBin = (function($) {
|
|
|
function getBlobUrl(data, mimeType)
|
|
|
{
|
|
|
// Transform into a Blob
|
|
|
- const buf = new Uint8Array(data.length);
|
|
|
- for (let i = 0; i < data.length; ++i) {
|
|
|
- buf[i] = data.charCodeAt(i);
|
|
|
- }
|
|
|
+ const buf = Uint8Array.from(
|
|
|
+ data,
|
|
|
+ character => character.charCodeAt(0)
|
|
|
+ );
|
|
|
const blob = new window.Blob(
|
|
|
[buf],
|
|
|
{
|
|
|
@@ -2999,22 +2994,22 @@ jQuery.PrivateBin = (function($) {
|
|
|
|
|
|
// We explicitly do _not_ use the original mime type for the download link
|
|
|
// to always force a download instead of potentially dangerous browser rendering/parsing/interpretation
|
|
|
- let safeMimeType = 'application/octet-stream';
|
|
|
+ let sanitizedMimeType = 'application/octet-stream';
|
|
|
if (me.isSafeMimeType(mimeType)) {
|
|
|
- safeMimeType = mimeType;
|
|
|
+ sanitizedMimeType = mimeType;
|
|
|
}
|
|
|
|
|
|
// extract data and convert to binary
|
|
|
- const rawData = attachmentData.substring(base64Start);
|
|
|
- const decodedData = rawData.length > 0 ? atob(rawData) : '';
|
|
|
+ const base64Data = attachmentData.substring(base64Start);
|
|
|
+ const plainData = base64Data.length > 0 ? atob(base64Data) : '';
|
|
|
|
|
|
- let blobUrl = getBlobUrl(decodedData, safeMimeType);
|
|
|
+ let blobUrl = getBlobUrl(plainData, sanitizedMimeType);
|
|
|
attachmentLink.attr('href', blobUrl);
|
|
|
|
|
|
if (typeof fileName !== 'undefined') {
|
|
|
attachmentLink.attr('download', fileName);
|
|
|
|
|
|
- const fileSize = Helper.formatBytes(decodedData.length);
|
|
|
+ const fileSize = Helper.formatBytes(plainData.length);
|
|
|
const spans = template[0].querySelectorAll('span');
|
|
|
const span = spans[spans.length - 1];
|
|
|
span.textContent += ` (${fileName}, ${fileSize})`;
|
|
|
@@ -3024,18 +3019,36 @@ jQuery.PrivateBin = (function($) {
|
|
|
// prevents executing embedded scripts when CSP is not set and user
|
|
|
// right-clicks/long-taps and opens the SVG in a new tab - prevented
|
|
|
// in the preview by use of an img tag, which disables scripts, too
|
|
|
- if (mimeType.match(/^image\/.*svg/i)) {
|
|
|
- const sanitizedData = DOMPurify.sanitize(
|
|
|
- decodedData,
|
|
|
- purifySvgConfig
|
|
|
- );
|
|
|
- blobUrl = getBlobUrl(sanitizedData, mimeType);
|
|
|
+ if (mimeType.startsWith('image\/svg')) {
|
|
|
+ try {
|
|
|
+ // attempt to UTF-8 decode the SVG data
|
|
|
+ const svgBuffer = Uint8Array.from(
|
|
|
+ plainData,
|
|
|
+ character => character.charCodeAt(0)
|
|
|
+ );
|
|
|
+ const utf8ValidatedSvgString = new TextDecoder(
|
|
|
+ 'utf-8',
|
|
|
+ {fatal: true}
|
|
|
+ ).decode(svgBuffer);
|
|
|
+ const sanitizedData = DOMPurify.sanitize(
|
|
|
+ utf8ValidatedSvgString,
|
|
|
+ purifySvgConfig
|
|
|
+ );
|
|
|
+ sanitizedMimeType = 'image/svg+xml';
|
|
|
+ blobUrl = getBlobUrl(sanitizedData, sanitizedMimeType);
|
|
|
+ } catch {
|
|
|
+ // Invalid or non-UTF-8 SVG: download only, no preview as it
|
|
|
+ // may be used to smuggle multi-byte sequences past DOMpurify
|
|
|
+ // such as `ļ` to get `\x01<` & `ľ` to get `\x01>`
|
|
|
+ sanitizedMimeType = 'application/octet-stream';
|
|
|
+ blobUrl = getBlobUrl(plainData, sanitizedMimeType);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
template.removeClass('hidden');
|
|
|
$attachment.append(template);
|
|
|
|
|
|
- me.handleBlobAttachmentPreview($attachmentPreview, blobUrl, mimeType);
|
|
|
+ me.handleBlobAttachmentPreview($attachmentPreview, blobUrl, sanitizedMimeType);
|
|
|
};
|
|
|
|
|
|
|
|
|
@@ -3043,21 +3056,23 @@ jQuery.PrivateBin = (function($) {
|
|
|
* Evaluates whether this is known a safe mime type.
|
|
|
*
|
|
|
* This means, the media can safely be displayed and e.g. no XSS should be possible.
|
|
|
- *
|
|
|
+ *
|
|
|
* @name AttachmentViewer.isSafeMimeType
|
|
|
* @function
|
|
|
* @param {string}
|
|
|
* @returns {bool}
|
|
|
*/
|
|
|
me.isSafeMimeType = function(mimeType) {
|
|
|
- return (
|
|
|
- mimeType.startsWith('image/') &&
|
|
|
+ return ((
|
|
|
+ mimeType.startsWith('image/') &&
|
|
|
!mimeType.includes('svg')
|
|
|
) ||
|
|
|
mimeType.startsWith('video/') ||
|
|
|
mimeType.startsWith('audio/') ||
|
|
|
- mimeType.endsWith('/pdf') ||
|
|
|
- mimeType === 'text/plain';
|
|
|
+ mimeType === 'application/pdf' ||
|
|
|
+ mimeType === 'text/plain') &&
|
|
|
+ // don't accept comments, stray characters, spaces, etc.
|
|
|
+ /^[a-z0-9][a-z0-9.-]*[a-z0-9]\/[a-z0-9][a-z0-9.+-]*[a-z0-9]$/.test(mimeType);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -3246,7 +3261,7 @@ jQuery.PrivateBin = (function($) {
|
|
|
const mimeTypeEnd = attachmentData.indexOf(';');
|
|
|
|
|
|
// extract mimeType
|
|
|
- return attachmentData.substring(5, mimeTypeEnd);
|
|
|
+ return attachmentData.substring(5, mimeTypeEnd).toLowerCase();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -3342,12 +3357,12 @@ jQuery.PrivateBin = (function($) {
|
|
|
const alreadyIncludesCurrentAttachment = $targetElement.find(`[src='${blobUrl}']`).length > 0;
|
|
|
|
|
|
if (blobUrl && !alreadyIncludesCurrentAttachment) {
|
|
|
- if (mimeType.toLowerCase().startsWith('image/')) {
|
|
|
+ if (mimeType.startsWith('image/')) {
|
|
|
const image = document.createElement('img');
|
|
|
image.setAttribute('src', blobUrl);
|
|
|
image.setAttribute('class', 'img-thumbnail');
|
|
|
$targetElement[0].appendChild(image);
|
|
|
- } else if (mimeType.toLowerCase().startsWith('video/')) {
|
|
|
+ } else if (mimeType.startsWith('video/')) {
|
|
|
const video = document.createElement('video');
|
|
|
video.setAttribute('controls', 'true');
|
|
|
video.setAttribute('autoplay', 'true');
|
|
|
@@ -3357,7 +3372,7 @@ jQuery.PrivateBin = (function($) {
|
|
|
source.setAttribute('src', blobUrl);
|
|
|
video.appendChild(source);
|
|
|
$targetElement[0].appendChild(video);
|
|
|
- } else if (mimeType.toLowerCase().startsWith('audio/')) {
|
|
|
+ } else if (mimeType.startsWith('audio/')) {
|
|
|
const audio = document.createElement('audio');
|
|
|
audio.setAttribute('controls', 'true');
|
|
|
audio.setAttribute('autoplay', 'true');
|
|
|
@@ -3366,7 +3381,7 @@ jQuery.PrivateBin = (function($) {
|
|
|
source.setAttribute('src', blobUrl);
|
|
|
audio.appendChild(source);
|
|
|
$targetElement[0].appendChild(audio);
|
|
|
- } else if (mimeType.toLowerCase().endsWith('/pdf')) {
|
|
|
+ } else if (mimeType === 'application/pdf') {
|
|
|
const embed = document.createElement('embed');
|
|
|
embed.setAttribute('src', blobUrl);
|
|
|
embed.setAttribute('type', 'application/pdf');
|