mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
fix(editor): Sanitize HTML binary-data before rendering in the UI (#7400)
This commit is contained in:
parent
47e8953ec9
commit
2b075bfc2d
|
@ -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'];
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue