-
-
-
@@ -145,7 +142,6 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
-
@@ -438,7 +434,6 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
-
Give it to me
@@ -516,7 +511,6 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
-
Solution steps:
@@ -1060,9 +1054,6 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
-
-
-
@@ -1076,7 +1067,6 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
-
@@ -1309,9 +1299,6 @@ exports[`AskAssistantChat > renders message with code snippet 1`] = `
-
-
-
@@ -1325,7 +1312,6 @@ exports[`AskAssistantChat > renders message with code snippet 1`] = `
-
renders streaming chat correctly 1`] = `
-
-
-
@@ -1568,7 +1551,6 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
-
diff --git a/packages/design-system/src/components/N8nActionBox/ActionBox.vue b/packages/design-system/src/components/N8nActionBox/ActionBox.vue
index c048c3f6438c5..edb877b3d650f 100644
--- a/packages/design-system/src/components/N8nActionBox/ActionBox.vue
+++ b/packages/design-system/src/components/N8nActionBox/ActionBox.vue
@@ -37,7 +37,7 @@ withDefaults(defineProps
(), {
-
+
@@ -61,7 +61,7 @@ withDefaults(defineProps(), {
:class="$style.callout"
>
-
+
diff --git a/packages/design-system/src/components/N8nInfoAccordion/InfoAccordion.vue b/packages/design-system/src/components/N8nInfoAccordion/InfoAccordion.vue
index 71830b02dcac6..e500c73cd65f1 100644
--- a/packages/design-system/src/components/N8nInfoAccordion/InfoAccordion.vue
+++ b/packages/design-system/src/components/N8nInfoAccordion/InfoAccordion.vue
@@ -75,7 +75,7 @@ const onTooltipClick = (item: string, event: MouseEvent) => emit('tooltipClick',
-
+
@@ -83,7 +83,7 @@ const onTooltipClick = (item: string, event: MouseEvent) => emit('tooltipClick',
-
+
diff --git a/packages/design-system/src/components/N8nInputLabel/InputLabel.vue b/packages/design-system/src/components/N8nInputLabel/InputLabel.vue
index 6c527549ed0f3..bda96c2e21feb 100644
--- a/packages/design-system/src/components/N8nInputLabel/InputLabel.vue
+++ b/packages/design-system/src/components/N8nInputLabel/InputLabel.vue
@@ -58,7 +58,7 @@ const addTargetBlank = (html: string) =>
-
+
diff --git a/packages/design-system/src/components/N8nMarkdown/Markdown.vue b/packages/design-system/src/components/N8nMarkdown/Markdown.vue
index c953ff9d657e5..45de891020bb3 100644
--- a/packages/design-system/src/components/N8nMarkdown/Markdown.vue
+++ b/packages/design-system/src/components/N8nMarkdown/Markdown.vue
@@ -202,7 +202,7 @@ const onCheckboxChange = (index: number) => {
@click="onClick"
@mousedown="onMouseDown"
@change="onChange"
- v-html="htmlContent"
+ v-n8n-html="htmlContent"
/>
diff --git a/packages/design-system/src/components/N8nMarkdown/__tests__/Markdown.spec.ts b/packages/design-system/src/components/N8nMarkdown/__tests__/Markdown.spec.ts
index 2c826f5192800..ac2faac7b308f 100644
--- a/packages/design-system/src/components/N8nMarkdown/__tests__/Markdown.spec.ts
+++ b/packages/design-system/src/components/N8nMarkdown/__tests__/Markdown.spec.ts
@@ -1,10 +1,16 @@
import { render, fireEvent } from '@testing-library/vue';
import N8nMarkdown from '../Markdown.vue';
+import { n8nHtml } from 'n8n-design-system/directives';
describe('components', () => {
describe('N8nMarkdown', () => {
it('should render unchecked checkboxes', () => {
const wrapper = render(N8nMarkdown, {
+ global: {
+ directives: {
+ n8nHtml,
+ },
+ },
props: {
content: '__TODO__\n- [ ] Buy milk\n- [ ] Buy socks\n',
},
@@ -18,6 +24,11 @@ describe('components', () => {
it('should render checked checkboxes', () => {
const wrapper = render(N8nMarkdown, {
+ global: {
+ directives: {
+ n8nHtml,
+ },
+ },
props: {
content: '__TODO__\n- [X] Buy milk\n- [X] Buy socks\n',
},
@@ -31,6 +42,11 @@ describe('components', () => {
it('should toggle checkboxes when clicked', async () => {
const wrapper = render(N8nMarkdown, {
+ global: {
+ directives: {
+ n8nHtml,
+ },
+ },
props: {
content: '__TODO__\n- [ ] Buy milk\n- [ ] Buy socks\n',
},
@@ -50,6 +66,11 @@ describe('components', () => {
it('should render inputs as plain text', () => {
const wrapper = render(N8nMarkdown, {
+ global: {
+ directives: {
+ n8nHtml,
+ },
+ },
props: {
content:
'__TODO__\n- [X] Buy milk\n- \n',
diff --git a/packages/design-system/src/components/N8nNotice/Notice.vue b/packages/design-system/src/components/N8nNotice/Notice.vue
index eb27aa2efdf8f..f630e95e59c77 100644
--- a/packages/design-system/src/components/N8nNotice/Notice.vue
+++ b/packages/design-system/src/components/N8nNotice/Notice.vue
@@ -73,7 +73,7 @@ const onClick = (event: MouseEvent) => {
:id="`${id}-content`"
:class="showFullContent ? $style['expanded'] : $style['truncated']"
role="region"
- v-html="displayContent"
+ v-n8n-html="displayContent"
/>
diff --git a/packages/design-system/src/components/N8nNotice/__tests__/Notice.spec.ts b/packages/design-system/src/components/N8nNotice/__tests__/Notice.spec.ts
index 84617dab515ec..180815e5367b0 100644
--- a/packages/design-system/src/components/N8nNotice/__tests__/Notice.spec.ts
+++ b/packages/design-system/src/components/N8nNotice/__tests__/Notice.spec.ts
@@ -1,6 +1,7 @@
import { render } from '@testing-library/vue';
import N8nNotice from '../Notice.vue';
import { N8nText } from 'n8n-design-system/components';
+import { n8nHtml } from 'n8n-design-system/directives';
describe('components', () => {
describe('N8nNotice', () => {
@@ -41,6 +42,9 @@ describe('components', () => {
content: 'Hello world! This is a notice.',
},
global: {
+ directives: {
+ n8nHtml,
+ },
components: {
'n8n-text': N8nText,
},
diff --git a/packages/design-system/src/components/N8nSticky/Sticky.vue b/packages/design-system/src/components/N8nSticky/Sticky.vue
index 4b7dca944acb0..9e258b4afa75a 100644
--- a/packages/design-system/src/components/N8nSticky/Sticky.vue
+++ b/packages/design-system/src/components/N8nSticky/Sticky.vue
@@ -116,7 +116,7 @@ const onInputScroll = (event: WheelEvent) => {
-
+
diff --git a/packages/design-system/src/components/N8nTabs/Tabs.vue b/packages/design-system/src/components/N8nTabs/Tabs.vue
index 64277ca8630e9..ff1b142e516e2 100644
--- a/packages/design-system/src/components/N8nTabs/Tabs.vue
+++ b/packages/design-system/src/components/N8nTabs/Tabs.vue
@@ -89,7 +89,7 @@ const scrollRight = () => scroll(50);
>
-
+
-
+
',
+};
+
+describe('Directive n8n-html', () => {
+ it('should sanitize html', async () => {
+ const { html } = render(TestComponent, {
+ props: {
+ html: 'text malicious ',
+ },
+ global: {
+ directives: {
+ n8nHtml,
+ },
+ },
+ });
+ expect(html()).toBe(
+ '',
+ );
+ });
+
+ it('should not touch safe html', async () => {
+ const { html } = render(TestComponent, {
+ props: {
+ html: 'text safe ',
+ },
+ global: {
+ directives: {
+ n8nHtml,
+ },
+ },
+ });
+ expect(html()).toBe(
+ '',
+ );
+ });
+});
diff --git a/packages/design-system/src/directives/n8n-html.ts b/packages/design-system/src/directives/n8n-html.ts
new file mode 100644
index 0000000000000..875905d1d9ff0
--- /dev/null
+++ b/packages/design-system/src/directives/n8n-html.ts
@@ -0,0 +1,37 @@
+import sanitize from 'sanitize-html';
+import type { DirectiveBinding, ObjectDirective } from 'vue';
+
+/**
+ * Custom directive `n8nHtml` to replace v-html from Vue to sanitize content.
+ *
+ * Usage:
+ * In your Vue template, use the directive `v-n8n-html` passing the unsafe HTML.
+ *
+ * Example:
+ * link'">
+ *
+ * Compiles to:
link
+ *
+ * Hint: Do not use it on components
+ * https://vuejs.org/guide/reusability/custom-directives#usage-on-components
+ */
+
+const configuredSanitize = (html: string) =>
+ sanitize(html, {
+ allowedTags: sanitize.defaults.allowedTags.concat(['img', 'input']),
+ allowedAttributes: {
+ ...sanitize.defaults.allowedAttributes,
+ input: ['type', 'id', 'checked'],
+ code: ['class'],
+ a: sanitize.defaults.allowedAttributes.a.concat(['data-*']),
+ },
+ });
+
+export const n8nHtml: ObjectDirective = {
+ beforeMount(el: HTMLElement, binding: DirectiveBinding) {
+ el.innerHTML = configuredSanitize(binding.value);
+ },
+ beforeUpdate(el: HTMLElement, binding: DirectiveBinding) {
+ el.innerHTML = configuredSanitize(binding.value);
+ },
+};
diff --git a/packages/editor-ui/src/components/Error/NodeErrorView.vue b/packages/editor-ui/src/components/Error/NodeErrorView.vue
index 0bc3bf5387d03..aa0eac4c69ff0 100644
--- a/packages/editor-ui/src/components/Error/NodeErrorView.vue
+++ b/packages/editor-ui/src/components/Error/NodeErrorView.vue
@@ -445,7 +445,7 @@ async function onAskAssistantClick() {
v-if="error.description || error.context?.descriptionKey"
data-test-id="node-error-description"
class="node-error-view__header-description"
- v-html="getErrorDescription()"
+ v-n8n-html="getErrorDescription()"
>