text
malicious',
+ };
+ },
+ template: '
',
+});
+
+describe('Directive n8n-html', () => {
+ it('should sanitize html', async () => {
+ const { html } = render(TestComponent, {
+ global: {
+ directives: {
+ 'n8n-html': 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..763ed61ce8578
--- /dev/null
+++ b/packages/design-system/src/directives/n8n-html.ts
@@ -0,0 +1,15 @@
+import sanitize from 'sanitize-html';
+import type { DirectiveBinding, ObjectDirective } from 'vue';
+
+const configuredSanitize = (html: string) => sanitize(html, {
+ allowedTags: sanitize.defaults.allowedTags.concat([ 'img' ])
+});
+
+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/design-system/src/main.ts b/packages/design-system/src/main.ts
index 54934d6f1c4d6..dddb89f888ff4 100644
--- a/packages/design-system/src/main.ts
+++ b/packages/design-system/src/main.ts
@@ -5,4 +5,5 @@ export * from './components';
export * from './plugin';
export * from './types';
export * from './utils';
+export * from './directives';
export { locale };
diff --git a/packages/design-system/src/plugin.ts b/packages/design-system/src/plugin.ts
index 493fde38ecc5a..c6d3d2a6c9f30 100644
--- a/packages/design-system/src/plugin.ts
+++ b/packages/design-system/src/plugin.ts
@@ -1,5 +1,6 @@
import type { Component, Plugin } from 'vue';
import * as components from './components';
+import * as directives from './directives';
export interface N8nPluginOptions {}
@@ -8,5 +9,9 @@ export const N8nPlugin: Plugin
= {
for (const [name, component] of Object.entries(components)) {
app.component(name, component as unknown as Component);
}
+
+ for (const [name, directive] of Object.entries(directives)) {
+ app.directive(name, directive);
+ }
},
};
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()"
>
@@ -68,7 +68,7 @@ export default defineComponent({
@click:button="openLinkPage"
>
-
+
diff --git a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.vue b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.vue
index 9e0744025c26b..73f12a0ec3852 100644
--- a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.vue
+++ b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.vue
@@ -115,15 +115,15 @@ watchDebounced(