mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Prevent clipboard xss injection (#10894)
This commit is contained in:
parent
48294e7ec1
commit
e20ab59c1d
|
@ -1,4 +1,4 @@
|
||||||
import { render, within } from '@testing-library/vue';
|
import { render } from '@testing-library/vue';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { defineComponent, h, ref } from 'vue';
|
import { defineComponent, h, ref } from 'vue';
|
||||||
import { useClipboard } from '@/composables/useClipboard';
|
import { useClipboard } from '@/composables/useClipboard';
|
||||||
|
@ -8,13 +8,9 @@ const testValue = 'This is a test';
|
||||||
const TestComponent = defineComponent({
|
const TestComponent = defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const pasted = ref('');
|
const pasted = ref('');
|
||||||
const htmlContent = ref<HTMLElement>();
|
|
||||||
const clipboard = useClipboard({
|
const clipboard = useClipboard({
|
||||||
onPaste(data) {
|
onPaste(data) {
|
||||||
pasted.value = data;
|
pasted.value = data;
|
||||||
if (htmlContent.value) {
|
|
||||||
htmlContent.value.innerHTML = data;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -27,7 +23,6 @@ const TestComponent = defineComponent({
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
h('div', { 'data-test-id': 'paste' }, pasted.value),
|
h('div', { 'data-test-id': 'paste' }, pasted.value),
|
||||||
h('div', { 'data-test-id': 'xss-attack', ref: htmlContent }),
|
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -73,12 +68,4 @@ describe('useClipboard()', () => {
|
||||||
expect(pasteElement.textContent).toEqual(testValue);
|
expect(pasteElement.textContent).toEqual(testValue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sanitizes HTML', async () => {
|
|
||||||
const unsafeHtml = 'https://www.ex.com/sfefdfd<img/src/onerror=alert(1)>fdf/xdfef.json';
|
|
||||||
const { getByTestId } = render(TestComponent);
|
|
||||||
|
|
||||||
await userEvent.paste(unsafeHtml);
|
|
||||||
expect(within(getByTestId('xss-attack')).queryByRole('img')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import { useClipboard as useClipboardCore } from '@vueuse/core';
|
import { useClipboard as useClipboardCore } from '@vueuse/core';
|
||||||
import { useDebounce } from '@/composables/useDebounce';
|
import { useDebounce } from '@/composables/useDebounce';
|
||||||
import { sanitizeIfString } from '@/utils/htmlUtils';
|
|
||||||
|
|
||||||
type ClipboardEventFn = (data: string, event?: ClipboardEvent) => void;
|
type ClipboardEventFn = (data: string, event?: ClipboardEvent) => void;
|
||||||
|
|
||||||
|
@ -43,7 +42,7 @@ export function useClipboard(
|
||||||
|
|
||||||
const clipboardData = event.clipboardData;
|
const clipboardData = event.clipboardData;
|
||||||
if (clipboardData !== null) {
|
if (clipboardData !== null) {
|
||||||
const clipboardValue = sanitizeIfString(clipboardData.getData('text/plain'));
|
const clipboardValue = clipboardData.getData('text/plain');
|
||||||
onPasteCallback.value(clipboardValue, event);
|
onPasteCallback.value(clipboardValue, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,4 +37,18 @@ describe('sanitizeHtml', () => {
|
||||||
const result = sanitizeHtml(dirtyHtml);
|
const result = sanitizeHtml(dirtyHtml);
|
||||||
expect(result).toBe('<a>Click me</a>');
|
expect(result).toBe('<a>Click me</a>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[
|
||||||
|
'https://www.ex.com/sfefdfd<img/src/onerror=alert(1)>fdf/xdfef.json',
|
||||||
|
'https://www.ex.com/sfefdfdfdf/xdfef.json',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// eslint-disable-next-line n8n-local-rules/no-unneeded-backticks
|
||||||
|
`https://www.ex.com/sfefdfd<details title='"><img/src/onerror=alert(document.domain)>/ '>/c.json`,
|
||||||
|
'https://www.ex.com/sfefdfd<details title=""><img/src/onerror=alert(document.domain)>/">/c.json',
|
||||||
|
],
|
||||||
|
])('should escape js code %s to equal %s', (dirtyURL, expected) => {
|
||||||
|
expect(sanitizeHtml(dirtyURL)).toBe(expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import xss, { friendlyAttrValue } from 'xss';
|
import xss, { escapeAttrValue } from 'xss';
|
||||||
import { ALLOWED_HTML_ATTRIBUTES, ALLOWED_HTML_TAGS } from '@/constants';
|
import { ALLOWED_HTML_ATTRIBUTES, ALLOWED_HTML_TAGS } from '@/constants';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -22,7 +22,7 @@ export function sanitizeHtml(dirtyHtml: string) {
|
||||||
if (name === 'href' && !value.match(/^https?:\/\//gm)) {
|
if (name === 'href' && !value.match(/^https?:\/\//gm)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return `${name}="${friendlyAttrValue(value)}"`;
|
return `${name}="${escapeAttrValue(value)}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
Loading…
Reference in a new issue