fix(editor): Sanitize HTML binary-data before rendering in the UI (#7400)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2023-10-11 12:09:19 +02:00 committed by GitHub
parent 47e8953ec9
commit 2b075bfc2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 31 additions and 21 deletions

View file

@ -7,16 +7,17 @@
<source :src="embedSource" :type="binaryData.mimeType" />
{{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }}
</video>
<audio v-if="binaryData.fileType === 'audio'" controls autoplay>
<audio v-else-if="binaryData.fileType === 'audio'" controls autoplay>
<source :src="embedSource" :type="binaryData.mimeType" />
{{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }}
</audio>
<vue-json-pretty
v-else-if="binaryData.fileType === 'json'"
:data="jsonData"
:data="data"
:deep="3"
:showLength="true"
/>
<run-data-html v-else-if="binaryData.fileType === 'html'" :inputHtml="data" />
<embed v-else :src="embedSource" class="binary-data" :class="embedClass()" />
</span>
</span>
@ -30,11 +31,13 @@ import { jsonParse } from 'n8n-workflow';
import type { PropType } from 'vue';
import VueJsonPretty from 'vue-json-pretty';
import { useWorkflowsStore } from '@/stores';
import RunDataHtml from '@/components/RunDataHtml.vue';
export default defineComponent({
name: 'BinaryDataDisplayEmbed',
components: {
VueJsonPretty,
RunDataHtml,
},
props: {
binaryData: {
@ -47,27 +50,29 @@ export default defineComponent({
isLoading: true,
embedSource: '',
error: false,
jsonData: '',
data: '',
};
},
computed: {
...mapStores(useWorkflowsStore),
},
async mounted() {
const { id, data, fileName, fileType, mimeType } = (this.binaryData || {}) as IBinaryData;
const { id, data, fileName, fileType, mimeType } = this.binaryData;
const isJSONData = fileType === 'json';
const isHTMLData = fileType === 'html';
if (!id) {
if (isJSONData) {
this.jsonData = jsonParse(atob(data));
if (isJSONData || isHTMLData) {
this.data = jsonParse(atob(data));
} else {
this.embedSource = 'data:' + mimeType + ';base64,' + data;
}
} else {
try {
const binaryUrl = this.workflowsStore.getBinaryUrl(id, 'view', fileName, mimeType);
if (isJSONData) {
this.jsonData = await (await fetch(binaryUrl)).json();
if (isJSONData || isHTMLData) {
const fetchedData = await fetch(binaryUrl, { credentials: 'include' });
this.data = await (isJSONData ? fetchedData.json() : fetchedData.text());
} else {
this.embedSource = binaryUrl;
}
@ -80,7 +85,7 @@ export default defineComponent({
},
methods: {
embedClass(): string[] {
const { fileType } = (this.binaryData || {}) as IBinaryData;
const { fileType } = this.binaryData;
return [fileType ?? 'other'];
},
},

View file

@ -347,7 +347,7 @@
</Suspense>
<Suspense v-else-if="hasNodeRun && isPaneTypeOutput && displayMode === 'html'">
<run-data-html :inputData="inputData" />
<run-data-html :inputHtml="inputData[0].json.html" />
</Suspense>
<Suspense v-else-if="hasNodeRun && isSchemaView">
@ -1320,7 +1320,9 @@ export default defineComponent({
},
isViewable(index: number, key: string): boolean {
const { fileType } = this.binaryData[index][key];
return !!fileType && ['image', 'audio', 'video', 'text', 'json', 'pdf'].includes(fileType);
return (
!!fileType && ['image', 'audio', 'video', 'text', 'json', 'pdf', 'html'].includes(fileType)
);
},
isDownloadable(index: number, key: string): boolean {
const { mimeType, fileName } = this.binaryData[index][key];

View file

@ -1,11 +1,10 @@
<template>
<iframe class="__html-display" :srcdoc="html" />
<iframe class="__html-display" :srcdoc="sanitizedHtml" />
</template>
<script lang="ts">
import type { PropType } from 'vue';
import sanitizeHtml, { defaults, type IOptions as SanitizeOptions } from 'sanitize-html';
import type { INodeExecutionData } from 'n8n-workflow';
const sanitizeOptions: SanitizeOptions = {
allowVulnerableTags: false,
@ -24,14 +23,13 @@ const sanitizeOptions: SanitizeOptions = {
export default {
name: 'RunDataHtml',
props: {
inputData: {
type: Array as PropType<INodeExecutionData[]>,
inputHtml: {
type: String as PropType<string>,
},
},
computed: {
html() {
const markup = (this.inputData?.[0].json.html as string) ?? '';
return sanitizeHtml(markup, sanitizeOptions);
sanitizedHtml() {
return sanitizeHtml(this.inputHtml, sanitizeOptions);
},
},
};

View file

@ -102,7 +102,7 @@ describe('Execute Spreadsheet File Node', () => {
binary: {
data: {
mimeType: 'text/html',
fileType: 'text',
fileType: 'html',
fileExtension: 'html',
data: 'PGh0bWw+PGhlYWQ+PG1ldGEgY2hhcnNldD0idXRmLTgiLz48dGl0bGU+U2hlZXRKUyBUYWJsZSBFeHBvcnQ8L3RpdGxlPjwvaGVhZD48Ym9keT48dGFibGU+PHRyPjx0ZCBkYXRhLXQ9InMiIGRhdGEtdj0iQSIgaWQ9InNqcy1BMSI+QTwvdGQ+PHRkIGRhdGEtdD0icyIgZGF0YS12PSJCIiBpZD0ic2pzLUIxIj5CPC90ZD48dGQgZGF0YS10PSJzIiBkYXRhLXY9IkMiIGlkPSJzanMtQzEiPkM8L3RkPjwvdHI+PHRyPjx0ZCBkYXRhLXQ9Im4iIGRhdGEtdj0iMSIgaWQ9InNqcy1BMiI+MTwvdGQ+PHRkIGRhdGEtdD0ibiIgZGF0YS12PSIyIiBpZD0ic2pzLUIyIj4yPC90ZD48dGQgZGF0YS10PSJuIiBkYXRhLXY9IjMiIGlkPSJzanMtQzIiPjM8L3RkPjwvdHI+PHRyPjx0ZCBkYXRhLXQ9Im4iIGRhdGEtdj0iNCIgaWQ9InNqcy1BMyI+NDwvdGQ+PHRkIGRhdGEtdD0ibiIgZGF0YS12PSI1IiBpZD0ic2pzLUIzIj41PC90ZD48dGQgZGF0YS10PSJuIiBkYXRhLXY9IjYiIGlkPSJzanMtQzMiPjY8L3RkPjwvdHI+PC90YWJsZT48L2JvZHk+PC9odG1sPg==',
fileName: 'spreadsheet.html',

View file

@ -35,7 +35,7 @@ export type IAllExecuteFunctions =
| ITriggerFunctions
| IWebhookFunctions;
export type BinaryFileType = 'text' | 'json' | 'image' | 'audio' | 'video' | 'pdf';
export type BinaryFileType = 'text' | 'json' | 'image' | 'audio' | 'video' | 'pdf' | 'html';
export interface IBinaryData {
[key: string]: string | undefined;
data: string;

View file

@ -112,6 +112,7 @@ export const sleep = async (ms: number): Promise<void> =>
export function fileTypeFromMimeType(mimeType: string): BinaryFileType | undefined {
if (mimeType.startsWith('application/json')) return 'json';
if (mimeType.startsWith('text/html')) return 'html';
if (mimeType.startsWith('image/')) return 'image';
if (mimeType.startsWith('audio/')) return 'audio';
if (mimeType.startsWith('video/')) return 'video';

View file

@ -196,6 +196,10 @@ describe('fileTypeFromMimeType', () => {
expect(fileTypeFromMimeType('application/json')).toEqual('json');
});
it('should recognize html', () => {
expect(fileTypeFromMimeType('text/html')).toEqual('html');
});
it('should recognize image', () => {
expect(fileTypeFromMimeType('image/jpeg')).toEqual('image');
expect(fileTypeFromMimeType('image/png')).toEqual('image');
@ -219,7 +223,7 @@ describe('fileTypeFromMimeType', () => {
it('should recognize text', () => {
expect(fileTypeFromMimeType('text/plain')).toEqual('text');
expect(fileTypeFromMimeType('text/css')).toEqual('text');
expect(fileTypeFromMimeType('text/html')).toEqual('text');
expect(fileTypeFromMimeType('text/html')).not.toEqual('text');
expect(fileTypeFromMimeType('text/javascript')).toEqual('text');
expect(fileTypeFromMimeType('application/javascript')).toEqual('text');
});