Merge pull request #5981 from matrix-org/jryans/upload-preview-mimetype

Adjust MIME type of upload confirmation if needed
This commit is contained in:
J. Ryan Stinnett 2021-05-10 10:21:35 +01:00 committed by GitHub
commit 45acf70b00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 110 additions and 80 deletions

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -16,20 +16,23 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import filesize from "filesize"; import filesize from "filesize";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { getBlobSafeMimeType } from '../../../utils/blobs';
interface IProps {
file: File;
currentIndex: number;
totalFiles?: number;
onFinished: (uploadConfirmed: boolean, uploadAll?: boolean) => void;
}
@replaceableComponent("views.dialogs.UploadConfirmDialog") @replaceableComponent("views.dialogs.UploadConfirmDialog")
export default class UploadConfirmDialog extends React.Component { export default class UploadConfirmDialog extends React.Component<IProps> {
static propTypes = { private objectUrl: string;
file: PropTypes.object.isRequired, private mimeType: string;
currentIndex: PropTypes.number,
totalFiles: PropTypes.number,
onFinished: PropTypes.func.isRequired,
}
static defaultProps = { static defaultProps = {
totalFiles: 1, totalFiles: 1,
@ -38,22 +41,28 @@ export default class UploadConfirmDialog extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this._objectUrl = URL.createObjectURL(props.file); // Create a fresh `Blob` for previewing (even though `File` already is
// one) so we can adjust the MIME type if needed.
this.mimeType = getBlobSafeMimeType(props.file.type);
const blob = new Blob([props.file], { type:
this.mimeType,
});
this.objectUrl = URL.createObjectURL(blob);
} }
componentWillUnmount() { componentWillUnmount() {
if (this._objectUrl) URL.revokeObjectURL(this._objectUrl); if (this.objectUrl) URL.revokeObjectURL(this.objectUrl);
} }
_onCancelClick = () => { private onCancelClick = () => {
this.props.onFinished(false); this.props.onFinished(false);
} }
_onUploadClick = () => { private onUploadClick = () => {
this.props.onFinished(true); this.props.onFinished(true);
} }
_onUploadAllClick = () => { private onUploadAllClick = () => {
this.props.onFinished(true, true); this.props.onFinished(true, true);
} }
@ -75,10 +84,10 @@ export default class UploadConfirmDialog extends React.Component {
} }
let preview; let preview;
if (this.props.file.type.startsWith('image/')) { if (this.mimeType.startsWith('image/')) {
preview = <div className="mx_UploadConfirmDialog_previewOuter"> preview = <div className="mx_UploadConfirmDialog_previewOuter">
<div className="mx_UploadConfirmDialog_previewInner"> <div className="mx_UploadConfirmDialog_previewInner">
<div><img className="mx_UploadConfirmDialog_imagePreview" src={this._objectUrl} /></div> <div><img className="mx_UploadConfirmDialog_imagePreview" src={this.objectUrl} /></div>
<div>{this.props.file.name} ({filesize(this.props.file.size)})</div> <div>{this.props.file.name} ({filesize(this.props.file.size)})</div>
</div> </div>
</div>; </div>;
@ -95,7 +104,7 @@ export default class UploadConfirmDialog extends React.Component {
let uploadAllButton; let uploadAllButton;
if (this.props.currentIndex + 1 < this.props.totalFiles) { if (this.props.currentIndex + 1 < this.props.totalFiles) {
uploadAllButton = <button onClick={this._onUploadAllClick}> uploadAllButton = <button onClick={this.onUploadAllClick}>
{_t("Upload all")} {_t("Upload all")}
</button>; </button>;
} }
@ -103,7 +112,7 @@ export default class UploadConfirmDialog extends React.Component {
return ( return (
<BaseDialog className='mx_UploadConfirmDialog' <BaseDialog className='mx_UploadConfirmDialog'
fixedWidth={false} fixedWidth={false}
onFinished={this._onCancelClick} onFinished={this.onCancelClick}
title={title} title={title}
contentId='mx_Dialog_content' contentId='mx_Dialog_content'
> >
@ -113,7 +122,7 @@ export default class UploadConfirmDialog extends React.Component {
<DialogButtons primaryButton={_t('Upload')} <DialogButtons primaryButton={_t('Upload')}
hasCancel={false} hasCancel={false}
onPrimaryButtonClick={this._onUploadClick} onPrimaryButtonClick={this.onUploadClick}
focus={true} focus={true}
> >
{uploadAllButton} {uploadAllButton}

View file

@ -18,62 +18,7 @@ limitations under the License.
import encrypt from 'browser-encrypt-attachment'; import encrypt from 'browser-encrypt-attachment';
import {mediaFromContent} from "../customisations/Media"; import {mediaFromContent} from "../customisations/Media";
import { IEncryptedFile } from "../customisations/models/IMediaEventContent"; import { IEncryptedFile } from "../customisations/models/IMediaEventContent";
import { getBlobSafeMimeType } from "./blobs";
// WARNING: We have to be very careful about what mime-types we allow into blobs,
// as for performance reasons these are now rendered via URL.createObjectURL()
// rather than by converting into data: URIs.
//
// This means that the content is rendered using the origin of the script which
// called createObjectURL(), and so if the content contains any scripting then it
// will pose a XSS vulnerability when the browser renders it. This is particularly
// bad if the user right-clicks the URI and pastes it into a new window or tab,
// as the blob will then execute with access to Element's full JS environment(!)
//
// See https://github.com/matrix-org/matrix-react-sdk/pull/1820#issuecomment-385210647
// for details.
//
// We mitigate this by only allowing mime-types into blobs which we know don't
// contain any scripting, and instantiate all others as application/octet-stream
// regardless of what mime-type the event claimed. Even if the payload itself
// is some malicious HTML, the fact we instantiate it with a media mimetype or
// application/octet-stream means the browser doesn't try to render it as such.
//
// One interesting edge case is image/svg+xml, which empirically *is* rendered
// correctly if the blob is set to the src attribute of an img tag (for thumbnails)
// *even if the mimetype is application/octet-stream*. However, empirically JS
// in the SVG isn't executed in this scenario, so we seem to be okay.
//
// Tested on Chrome 65 and Firefox 60
//
// The list below is taken mainly from
// https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
// N.B. Matrix doesn't currently specify which mimetypes are valid in given
// events, so we pick the ones which HTML5 browsers should be able to display
//
// For the record, mime-types which must NEVER enter this list below include:
// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar.
const ALLOWED_BLOB_MIMETYPES = [
'image/jpeg',
'image/gif',
'image/png',
'video/mp4',
'video/webm',
'video/ogg',
'audio/mp4',
'audio/webm',
'audio/aac',
'audio/mpeg',
'audio/ogg',
'audio/wave',
'audio/wav',
'audio/x-wav',
'audio/x-pn-wav',
'audio/flac',
'audio/x-flac',
];
/** /**
* Decrypt a file attached to a matrix event. * Decrypt a file attached to a matrix event.
@ -100,9 +45,7 @@ export function decryptFile(file: IEncryptedFile): Promise<Blob> {
// browser (e.g. by copying the URI into a new tab or window.) // browser (e.g. by copying the URI into a new tab or window.)
// See warning at top of file. // See warning at top of file.
let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : ''; let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : '';
if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) { mimetype = getBlobSafeMimeType(mimetype);
mimetype = 'application/octet-stream';
}
return new Blob([dataArray], {type: mimetype}); return new Blob([dataArray], {type: mimetype});
}); });

78
src/utils/blobs.ts Normal file
View file

@ -0,0 +1,78 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// WARNING: We have to be very careful about what mime-types we allow into blobs,
// as for performance reasons these are now rendered via URL.createObjectURL()
// rather than by converting into data: URIs.
//
// This means that the content is rendered using the origin of the script which
// called createObjectURL(), and so if the content contains any scripting then it
// will pose a XSS vulnerability when the browser renders it. This is particularly
// bad if the user right-clicks the URI and pastes it into a new window or tab,
// as the blob will then execute with access to Element's full JS environment(!)
//
// See https://github.com/matrix-org/matrix-react-sdk/pull/1820#issuecomment-385210647
// for details.
//
// We mitigate this by only allowing mime-types into blobs which we know don't
// contain any scripting, and instantiate all others as application/octet-stream
// regardless of what mime-type the event claimed. Even if the payload itself
// is some malicious HTML, the fact we instantiate it with a media mimetype or
// application/octet-stream means the browser doesn't try to render it as such.
//
// One interesting edge case is image/svg+xml, which empirically *is* rendered
// correctly if the blob is set to the src attribute of an img tag (for thumbnails)
// *even if the mimetype is application/octet-stream*. However, empirically JS
// in the SVG isn't executed in this scenario, so we seem to be okay.
//
// Tested on Chrome 65 and Firefox 60
//
// The list below is taken mainly from
// https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
// N.B. Matrix doesn't currently specify which mimetypes are valid in given
// events, so we pick the ones which HTML5 browsers should be able to display
//
// For the record, mime-types which must NEVER enter this list below include:
// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar.
const ALLOWED_BLOB_MIMETYPES = [
'image/jpeg',
'image/gif',
'image/png',
'video/mp4',
'video/webm',
'video/ogg',
'audio/mp4',
'audio/webm',
'audio/aac',
'audio/mpeg',
'audio/ogg',
'audio/wave',
'audio/wav',
'audio/x-wav',
'audio/x-pn-wav',
'audio/flac',
'audio/x-flac',
];
export function getBlobSafeMimeType(mimetype: string): string {
if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
return 'application/octet-stream';
}
return mimetype;
}