mirror of
https://github.com/danog/telegram-tt.git
synced 2024-11-27 04:45:08 +01:00
Proper fix for HTML-in-video attacks (#1387)
This commit is contained in:
parent
262bf7982d
commit
9cc050b766
@ -225,7 +225,7 @@ export async function invokeRequest<T extends GramJs.AnyRequest>(
|
||||
}
|
||||
|
||||
export function downloadMedia(
|
||||
args: { url: string; mediaFormat: ApiMediaFormat; start?: number; end?: number },
|
||||
args: { url: string; mediaFormat: ApiMediaFormat; start?: number; end?: number; isHtmlAllowed?: boolean },
|
||||
onProgress?: ApiOnProgress,
|
||||
) {
|
||||
return downloadMediaWithClient(args, client, isConnected, onProgress);
|
||||
|
@ -11,11 +11,10 @@ import {
|
||||
MEDIA_CACHE_MAX_BYTES,
|
||||
MEDIA_CACHE_NAME,
|
||||
MEDIA_CACHE_NAME_AVATARS,
|
||||
TRANSPARENT_PIXEL,
|
||||
} from '../../../config';
|
||||
import localDb from '../localDb';
|
||||
import { getEntityTypeById } from '../gramjsBuilders';
|
||||
import { blobToDataUri, dataUriToBlob } from '../../../util/files';
|
||||
import { blobToDataUri } from '../../../util/files';
|
||||
import * as cacheApi from '../../../util/cacheApi';
|
||||
|
||||
type EntityType = (
|
||||
@ -25,9 +24,9 @@ const MEDIA_ENTITY_TYPES = new Set(['msg', 'sticker', 'gif', 'wallpaper', 'photo
|
||||
|
||||
export default async function downloadMedia(
|
||||
{
|
||||
url, mediaFormat, start, end,
|
||||
url, mediaFormat, start, end, isHtmlAllowed,
|
||||
}: {
|
||||
url: string; mediaFormat: ApiMediaFormat; start?: number; end?: number;
|
||||
url: string; mediaFormat: ApiMediaFormat; start?: number; end?: number; isHtmlAllowed?: boolean;
|
||||
},
|
||||
client: TelegramClient,
|
||||
isConnected: boolean,
|
||||
@ -35,7 +34,7 @@ export default async function downloadMedia(
|
||||
) {
|
||||
const {
|
||||
data, mimeType, fullSize,
|
||||
} = await download(url, client, isConnected, onProgress, start, end, mediaFormat) || {};
|
||||
} = await download(url, client, isConnected, onProgress, start, end, mediaFormat, isHtmlAllowed) || {};
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
@ -73,6 +72,7 @@ async function download(
|
||||
start?: number,
|
||||
end?: number,
|
||||
mediaFormat?: ApiMediaFormat,
|
||||
isHtmlAllowed?: boolean,
|
||||
) {
|
||||
const mediaMatch = url.startsWith('webDocument')
|
||||
? url.match(/(webDocument):(.+)/)
|
||||
@ -169,6 +169,11 @@ async function download(
|
||||
fullSize = (entity as GramJs.Document).size;
|
||||
}
|
||||
|
||||
// Prevent HTML-in-video attacks
|
||||
if (!isHtmlAllowed && mimeType) {
|
||||
mimeType = mimeType.replace(/html/gi, '');
|
||||
}
|
||||
|
||||
return { mimeType, data, fullSize };
|
||||
} else if (entityType === 'stickerSet') {
|
||||
const data = await client.downloadStickerSetThumb(entity);
|
||||
@ -234,11 +239,6 @@ async function parseMedia(
|
||||
|
||||
function prepareMedia(mediaData: ApiParsedMedia): ApiPreparedMedia {
|
||||
if (mediaData instanceof Blob) {
|
||||
// Prevent HTML-in-video attacks
|
||||
if (mediaData.type.includes('text/html')) {
|
||||
return URL.createObjectURL(dataUriToBlob(TRANSPARENT_PIXEL));
|
||||
}
|
||||
|
||||
return URL.createObjectURL(mediaData);
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ import React, {
|
||||
FC, useCallback, useEffect, useState, memo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import { ApiMessage } from '../../api/types';
|
||||
import { ApiMediaFormat, ApiMessage } from '../../api/types';
|
||||
|
||||
import { getDocumentExtension, getDocumentHasPreview } from './helpers/documentInfo';
|
||||
import {
|
||||
@ -62,7 +62,9 @@ const Document: FC<OwnProps> = ({
|
||||
const [isDownloadAllowed, setIsDownloadAllowed] = useState(false);
|
||||
const {
|
||||
mediaData, downloadProgress,
|
||||
} = useMediaWithDownloadProgress(getMessageMediaHash(message, 'download'), !isDownloadAllowed);
|
||||
} = useMediaWithDownloadProgress<ApiMediaFormat.BlobUrl>(
|
||||
getMessageMediaHash(message, 'download'), !isDownloadAllowed, undefined, undefined, undefined, true,
|
||||
);
|
||||
const {
|
||||
isUploading, isTransferring, transferProgress,
|
||||
} = getMediaTransferState(message, uploadProgress || downloadProgress, isDownloadAllowed);
|
||||
|
@ -32,7 +32,6 @@ export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
|
||||
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
|
||||
export const LANG_CACHE_NAME = 'tt-lang-packs-v5';
|
||||
export const ASSET_CACHE_NAME = 'tt-assets';
|
||||
export const TRANSPARENT_PIXEL = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';
|
||||
|
||||
export const DOWNLOAD_WORKERS = 16;
|
||||
export const UPLOAD_WORKERS = 16;
|
||||
|
@ -19,6 +19,7 @@ export default <T extends ApiMediaFormat = ApiMediaFormat.BlobUrl>(
|
||||
mediaFormat: T = ApiMediaFormat.BlobUrl,
|
||||
cacheBuster?: number,
|
||||
delay?: number | false,
|
||||
isHtmlAllowed = false,
|
||||
) => {
|
||||
const mediaData = mediaHash ? mediaLoader.getFromMemory<T>(mediaHash) : undefined;
|
||||
const isStreaming = mediaFormat === ApiMediaFormat.Stream || (
|
||||
@ -47,7 +48,7 @@ export default <T extends ApiMediaFormat = ApiMediaFormat.BlobUrl>(
|
||||
|
||||
startedAtRef.current = Date.now();
|
||||
|
||||
mediaLoader.fetch(mediaHash, mediaFormat, handleProgress).then(() => {
|
||||
mediaLoader.fetch(mediaHash, mediaFormat, isHtmlAllowed, handleProgress).then(() => {
|
||||
const spentTime = Date.now() - startedAtRef.current!;
|
||||
startedAtRef.current = undefined;
|
||||
|
||||
@ -63,7 +64,10 @@ export default <T extends ApiMediaFormat = ApiMediaFormat.BlobUrl>(
|
||||
}, STREAMING_TIMEOUT);
|
||||
}
|
||||
}
|
||||
}, [noLoad, mediaHash, mediaData, mediaFormat, cacheBuster, forceUpdate, isStreaming, delay, handleProgress]);
|
||||
}, [
|
||||
noLoad, mediaHash, mediaData, mediaFormat, cacheBuster, forceUpdate, isStreaming, delay, handleProgress,
|
||||
isHtmlAllowed,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (noLoad && startedAtRef.current) {
|
||||
|
@ -7,7 +7,9 @@ export enum Type {
|
||||
Json,
|
||||
}
|
||||
|
||||
export async function fetch(cacheName: string, key: string, type: Type) {
|
||||
export async function fetch(
|
||||
cacheName: string, key: string, type: Type, isHtmlAllowed = false,
|
||||
) {
|
||||
if (!cacheApi) {
|
||||
return undefined;
|
||||
}
|
||||
@ -36,10 +38,15 @@ export async function fetch(cacheName: string, key: string, type: Type) {
|
||||
if (!blob.type) {
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
if (contentType) {
|
||||
return new Blob([blob], { type: contentType });
|
||||
return new Blob([blob], { type: isHtmlAllowed ? contentType : contentType.replace(/html/gi, '') });
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent HTML-in-video attacks (for files that were cached before fix)
|
||||
if (!isHtmlAllowed && blob.type.includes('html')) {
|
||||
return new Blob([blob], { type: blob.type.replace(/html/gi, '') });
|
||||
}
|
||||
|
||||
return blob;
|
||||
}
|
||||
case Type.Json:
|
||||
|
@ -7,11 +7,11 @@ import {
|
||||
} from '../api/types';
|
||||
|
||||
import {
|
||||
DEBUG, MEDIA_CACHE_DISABLED, MEDIA_CACHE_NAME, MEDIA_CACHE_NAME_AVATARS, TRANSPARENT_PIXEL,
|
||||
DEBUG, MEDIA_CACHE_DISABLED, MEDIA_CACHE_NAME, MEDIA_CACHE_NAME_AVATARS,
|
||||
} from '../config';
|
||||
import { callApi, cancelApiProgress } from '../api/gramjs';
|
||||
import * as cacheApi from './cacheApi';
|
||||
import { dataUriToBlob, fetchBlob } from './files';
|
||||
import { fetchBlob } from './files';
|
||||
import { IS_OPUS_SUPPORTED, IS_PROGRESSIVE_SUPPORTED, isWebpSupported } from './environment';
|
||||
import { oggToWav } from './oggToWav';
|
||||
import { webpToPng } from './webpToPng';
|
||||
@ -30,18 +30,18 @@ const memoryCache = new Map<string, ApiPreparedMedia>();
|
||||
const fetchPromises = new Map<string, Promise<ApiPreparedMedia | undefined>>();
|
||||
|
||||
export function fetch<T extends ApiMediaFormat>(
|
||||
url: string, mediaFormat: T, onProgress?: ApiOnProgress,
|
||||
url: string, mediaFormat: T, isHtmlAllowed = false, onProgress?: ApiOnProgress,
|
||||
): Promise<ApiMediaFormatToPrepared<T>> {
|
||||
if (mediaFormat === ApiMediaFormat.Progressive) {
|
||||
return (
|
||||
IS_PROGRESSIVE_SUPPORTED
|
||||
? getProgressive(url)
|
||||
: fetch(url, ApiMediaFormat.BlobUrl, onProgress)
|
||||
: fetch(url, ApiMediaFormat.BlobUrl, isHtmlAllowed, onProgress)
|
||||
) as Promise<ApiMediaFormatToPrepared<T>>;
|
||||
}
|
||||
|
||||
if (!fetchPromises.has(url)) {
|
||||
const promise = fetchFromCacheOrRemote(url, mediaFormat, onProgress)
|
||||
const promise = fetchFromCacheOrRemote(url, mediaFormat, isHtmlAllowed, onProgress)
|
||||
.catch((err) => {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -76,10 +76,12 @@ function getProgressive(url: string) {
|
||||
return Promise.resolve(progressiveUrl);
|
||||
}
|
||||
|
||||
async function fetchFromCacheOrRemote(url: string, mediaFormat: ApiMediaFormat, onProgress?: ApiOnProgress) {
|
||||
async function fetchFromCacheOrRemote(
|
||||
url: string, mediaFormat: ApiMediaFormat, isHtmlAllowed: boolean, onProgress?: ApiOnProgress,
|
||||
) {
|
||||
if (!MEDIA_CACHE_DISABLED) {
|
||||
const cacheName = url.startsWith('avatar') ? MEDIA_CACHE_NAME_AVATARS : MEDIA_CACHE_NAME;
|
||||
const cached = await cacheApi.fetch(cacheName, url, asCacheApiType[mediaFormat]!);
|
||||
const cached = await cacheApi.fetch(cacheName, url, asCacheApiType[mediaFormat]!, isHtmlAllowed);
|
||||
if (cached) {
|
||||
let media = cached;
|
||||
|
||||
@ -136,7 +138,7 @@ async function fetchFromCacheOrRemote(url: string, mediaFormat: ApiMediaFormat,
|
||||
return streamUrl;
|
||||
}
|
||||
|
||||
const remote = await callApi('downloadMedia', { url, mediaFormat }, onProgress);
|
||||
const remote = await callApi('downloadMedia', { url, mediaFormat, isHtmlAllowed }, onProgress);
|
||||
if (!remote) {
|
||||
throw new Error('Failed to fetch media');
|
||||
}
|
||||
@ -167,11 +169,6 @@ async function fetchFromCacheOrRemote(url: string, mediaFormat: ApiMediaFormat,
|
||||
|
||||
function prepareMedia(mediaData: ApiParsedMedia): ApiPreparedMedia {
|
||||
if (mediaData instanceof Blob) {
|
||||
// Prevent HTML-in-video attacks
|
||||
if (mediaData.type.includes('text/html')) {
|
||||
return URL.createObjectURL(dataUriToBlob(TRANSPARENT_PIXEL));
|
||||
}
|
||||
|
||||
return URL.createObjectURL(mediaData);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user