diff --git a/packages/editor-ui/src/composables/__tests__/useClipboard.test.ts b/packages/editor-ui/src/composables/__tests__/useClipboard.test.ts index 6c4b977a0b707..cf2637fb5a586 100644 --- a/packages/editor-ui/src/composables/__tests__/useClipboard.test.ts +++ b/packages/editor-ui/src/composables/__tests__/useClipboard.test.ts @@ -1,4 +1,4 @@ -import { render } from '@testing-library/vue'; +import { render, within } from '@testing-library/vue'; import userEvent from '@testing-library/user-event'; import { defineComponent, h, ref } from 'vue'; import { useClipboard } from '@/composables/useClipboard'; @@ -8,9 +8,13 @@ const testValue = 'This is a test'; const TestComponent = defineComponent({ setup() { const pasted = ref(''); + const htmlContent = ref(); const clipboard = useClipboard({ onPaste(data) { pasted.value = data; + if (htmlContent.value) { + htmlContent.value.innerHTML = data; + } }, }); @@ -23,6 +27,7 @@ const TestComponent = defineComponent({ }, }), h('div', { 'data-test-id': 'paste' }, pasted.value), + h('div', { 'data-test-id': 'xss-attack', ref: htmlContent }), ]); }, }); @@ -68,4 +73,12 @@ describe('useClipboard()', () => { expect(pasteElement.textContent).toEqual(testValue); }); }); + + it('sanitizes HTML', async () => { + const unsafeHtml = 'https://www.ex.com/sfefdfdfdf/xdfef.json'; + const { getByTestId } = render(TestComponent); + + await userEvent.paste(unsafeHtml); + expect(within(getByTestId('xss-attack')).queryByRole('img')).not.toBeInTheDocument(); + }); }); diff --git a/packages/editor-ui/src/composables/useClipboard.ts b/packages/editor-ui/src/composables/useClipboard.ts index 1cd4c9ef709ec..9401b95c45cd1 100644 --- a/packages/editor-ui/src/composables/useClipboard.ts +++ b/packages/editor-ui/src/composables/useClipboard.ts @@ -1,6 +1,7 @@ import { onBeforeUnmount, onMounted, ref } from 'vue'; import { useClipboard as useClipboardCore } from '@vueuse/core'; import { useDebounce } from '@/composables/useDebounce'; +import { sanitizeIfString } from '@/utils/htmlUtils'; type ClipboardEventFn = (data: string, event?: ClipboardEvent) => void; @@ -42,7 +43,7 @@ export function useClipboard( const clipboardData = event.clipboardData; if (clipboardData !== null) { - const clipboardValue = clipboardData.getData('text/plain'); + const clipboardValue = sanitizeIfString(clipboardData.getData('text/plain')); onPasteCallback.value(clipboardValue, event); } } diff --git a/packages/editor-ui/src/composables/useMessage.ts b/packages/editor-ui/src/composables/useMessage.ts index e24dc4579b706..22bb50fc698c4 100644 --- a/packages/editor-ui/src/composables/useMessage.ts +++ b/packages/editor-ui/src/composables/useMessage.ts @@ -1,5 +1,6 @@ import type { ElMessageBoxOptions, Action, MessageBoxInputData } from 'element-plus'; import { ElMessageBox as MessageBox } from 'element-plus'; +import { sanitizeIfString } from '@/utils/htmlUtils'; export type MessageBoxConfirmResult = 'confirm' | 'cancel'; @@ -28,11 +29,13 @@ export function useMessage() { }; if (typeof configOrTitle === 'string') { - return await MessageBox.alert(message, configOrTitle, resolvedConfig).catch( + return await MessageBox.alert(sanitizeIfString(message), configOrTitle, resolvedConfig).catch( handleCancelOrClose, ); } - return await MessageBox.alert(message, resolvedConfig).catch(handleCancelOrClose); + return await MessageBox.alert(sanitizeIfString(message), resolvedConfig).catch( + handleCancelOrClose, + ); } async function confirm( @@ -50,12 +53,16 @@ export function useMessage() { }; if (typeof configOrTitle === 'string') { - return await MessageBox.confirm(message, configOrTitle, resolvedConfig).catch( - handleCancelOrClose, - ); + return await MessageBox.confirm( + sanitizeIfString(message), + sanitizeIfString(configOrTitle), + resolvedConfig, + ).catch(handleCancelOrClose); } - return await MessageBox.confirm(message, resolvedConfig).catch(handleCancelOrClose); + return await MessageBox.confirm(sanitizeIfString(message), resolvedConfig).catch( + handleCancelOrClose, + ); } async function prompt( @@ -70,11 +77,15 @@ export function useMessage() { }; if (typeof configOrTitle === 'string') { - return await MessageBox.prompt(message, configOrTitle, resolvedConfig).catch( - handleCancelOrClosePrompt, - ); + return await MessageBox.prompt( + sanitizeIfString(message), + sanitizeIfString(configOrTitle), + resolvedConfig, + ).catch(handleCancelOrClosePrompt); } - return await MessageBox.prompt(message, resolvedConfig).catch(handleCancelOrClosePrompt); + return await MessageBox.prompt(sanitizeIfString(message), resolvedConfig).catch( + handleCancelOrClosePrompt, + ); } return { diff --git a/packages/editor-ui/src/utils/htmlUtils.ts b/packages/editor-ui/src/utils/htmlUtils.ts index 5da34b396526b..2a78582b016a1 100644 --- a/packages/editor-ui/src/utils/htmlUtils.ts +++ b/packages/editor-ui/src/utils/htmlUtils.ts @@ -37,6 +37,17 @@ export function sanitizeHtml(dirtyHtml: string) { return sanitizedHtml; } +/** + * Checks if the input is a string and sanitizes it by removing or escaping harmful characters, + * returning the original input if it's not a string. + */ +export const sanitizeIfString = (message: T): string | T => { + if (typeof message === 'string') { + return sanitizeHtml(message); + } + return message; +}; + export function setPageTitle(title: string) { window.document.title = title; }