Skip to content

Commit

Permalink
fix(editor): Replace v-html with custom directive to sanitize content
Browse files Browse the repository at this point in the history
  • Loading branch information
cstuncsik committed Sep 13, 2024
1 parent 5156313 commit c93e33c
Show file tree
Hide file tree
Showing 43 changed files with 109 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ function growInput() {
</div>
<div :class="$style.blockBody">
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="renderMarkdown(message.content)"></span>
<span v-n8n-html="renderMarkdown(message.content)"></span>
<BlinkingCursor
v-if="streaming && i === messages?.length - 1 && message.title && message.content"
/>
Expand All @@ -161,17 +161,17 @@ function growInput() {
</div>
<div v-else-if="message.type === 'text'" :class="$style.textMessage">
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-if="message.role === 'user'" v-html="renderMarkdown(message.content)"></span>
<span v-if="message.role === 'user'" v-n8n-html="renderMarkdown(message.content)"></span>
<!-- eslint-disable-next-line vue/no-v-html -->
<div
v-else
:class="$style.assistantText"
v-html="renderMarkdown(message.content)"
v-n8n-html="renderMarkdown(message.content)"
></div>
<div
v-if="message?.codeSnippet"
:class="$style['code-snippet']"
v-html="renderMarkdown(message.codeSnippet).trim()"
v-n8n-html="renderMarkdown(message.codeSnippet).trim()"
></div>
<BlinkingCursor
v-if="streaming && i === messages?.length - 1 && message.role === 'assistant'"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ withDefaults(defineProps<ActionBoxProps>(), {
<div :class="$style.description" @click="$emit('descriptionClick', $event)">
<N8nText color="text-base">
<slot name="description">
<span v-html="description"></span>
<span v-n8n-html="description"></span>
</slot>
</N8nText>
</div>
Expand All @@ -61,7 +61,7 @@ withDefaults(defineProps<ActionBoxProps>(), {
:class="$style.callout"
>
<N8nText color="text-base">
<span size="small" v-html="calloutText"></span>
<span size="small" v-n8n-html="calloutText"></span>
</N8nText>
</N8nCallout>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,15 @@ const onTooltipClick = (item: string, event: MouseEvent) => emit('tooltipClick',
<div v-for="item in items" :key="item.id" :class="$style.accordionItem">
<n8n-tooltip :disabled="!item.tooltip">
<template #content>
<div @click="onTooltipClick(item.id, $event)" v-html="item.tooltip"></div>
<div @click="onTooltipClick(item.id, $event)" v-n8n-html="item.tooltip"></div>
</template>
<N8nIcon :icon="item.icon" :color="item.iconColor" size="small" class="mr-2xs" />
</n8n-tooltip>
<N8nText size="small" color="text-base">{{ item.label }}</N8nText>
</div>
</div>
<N8nText color="text-base" size="small" align="left">
<span v-html="description"></span>
<span v-n8n-html="description"></span>
</N8nText>
<slot name="customContent"></slot>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const addTargetBlank = (html: string) =>
<N8nTooltip placement="top" :popper-class="$style.tooltipPopper" :show-after="300">
<N8nIcon icon="question-circle" size="small" />
<template #content>
<div v-html="addTargetBlank(tooltipText)" />
<div v-n8n-html="addTargetBlank(tooltipText)" />
</template>
</N8nTooltip>
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ const onCheckboxChange = (index: number) => {
@click="onClick"
@mousedown="onMouseDown"
@change="onChange"
v-html="htmlContent"
v-n8n-html="htmlContent"
/>
<div v-else :class="$style.markdown">
<div v-for="(_, index) in loadingBlocks" :key="index">
Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/components/N8nNotice/Notice.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/>
</slot>
</N8nText>
Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/components/N8nSticky/Sticky.vue
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const onInputScroll = (event: WheelEvent) => {
</div>
<div v-if="editMode && shouldShowFooter" :class="$style.footer">
<N8nText size="xsmall" align="right">
<span v-html="t('sticky.markdownHint')"></span>
<span v-n8n-html="t('sticky.markdownHint')"></span>
</N8nText>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/components/N8nTabs/Tabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const scrollRight = () => scroll(50);
>
<N8nTooltip :disabled="!option.tooltip" placement="bottom">
<template #content>
<div @click="handleTooltipClick(option.value, $event)" v-html="option.tooltip" />
<div @click="handleTooltipClick(option.value, $event)" v-n8n-html="option.tooltip" />
</template>
<a
v-if="option.href"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ defineOptions({
<slot />
<template #content>
<slot name="content">
<div v-html="props.content"></div>
<div v-n8n-html="props.content"></div>
</slot>
<div
v-if="props.buttons.length"
Expand Down
1 change: 1 addition & 0 deletions packages/design-system/src/directives/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { n8nHtml } from './n8n-html';
28 changes: 28 additions & 0 deletions packages/design-system/src/directives/n8n-html.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { render } from '@testing-library/vue';
import { defineComponent } from 'vue';
import { n8nHtml } from './n8n-html';

const TestComponent = defineComponent({
setup() {
return {
unsafeHtml:
'<span>text</span><a href="https://malicious.com" onclick="alert(1)">malicious</a><img alt="Ok" src="./images/logo.svg" onerror="alert(2)<script>alert(3)</script>" />',
};
},
template: '<div v-n8n-html="unsafeHtml"></div>',
});

describe('Directive n8n-html', () => {
it('should sanitize html', async () => {
const { html } = render(TestComponent, {
global: {
directives: {
'n8n-html': n8nHtml,
},
},
});
expect(html()).toBe(
'<div><span>text</span><a href="https://malicious.com">malicious</a><img alt="Ok" src="./images/logo.svg"></div>',
);
});
});
15 changes: 15 additions & 0 deletions packages/design-system/src/directives/n8n-html.ts
Original file line number Diff line number Diff line change
@@ -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);
},
};
1 change: 1 addition & 0 deletions packages/design-system/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export * from './components';
export * from './plugin';
export * from './types';
export * from './utils';
export * from './directives';
export { locale };
5 changes: 5 additions & 0 deletions packages/design-system/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Component, Plugin } from 'vue';
import * as components from './components';
import * as directives from './directives';

export interface N8nPluginOptions {}

Expand All @@ -8,5 +9,9 @@ export const N8nPlugin: Plugin<N8nPluginOptions> = {
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);
}
},
};
2 changes: 1 addition & 1 deletion packages/editor-ui/src/components/Error/NodeErrorView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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()"
></div>
<div
v-if="isAskAssistantAvailable"
Expand Down
2 changes: 1 addition & 1 deletion packages/editor-ui/src/components/ExpressionEditModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ async function onDrop(expression: string, event: MouseEvent) {
<N8nText
:class="$style.tip"
size="small"
v-html="i18n.baseText('expressionTip.javascript')"
v-n8n-html="i18n.baseText('expressionTip.javascript')"
/>
</div>

Expand Down
4 changes: 2 additions & 2 deletions packages/editor-ui/src/components/FeatureComingSoon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default defineComponent({
</div>
<div v-if="featureInfo.infoText" class="mb-l">
<n8n-info-tip theme="info" type="note">
<span v-html="$locale.baseText(featureInfo.infoText)"></span>
<span v-n8n-html="$locale.baseText(featureInfo.infoText)"></span>
</n8n-info-tip>
</div>
<div :class="$style.actionBoxContainer">
Expand All @@ -68,7 +68,7 @@ export default defineComponent({
@click:button="openLinkPage"
>
<template #heading>
<span v-html="$locale.baseText(featureInfo.actionBoxTitle)" />
<span v-n8n-html="$locale.baseText(featureInfo.actionBoxTitle)" />
</template>
</n8n-action-box>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,15 @@ watchDebounced(
</div>

<div v-else-if="tip === 'dotPrimitive'" :class="$style.content">
<span v-html="i18n.baseText('expressionTip.typeDotPrimitive')" />
<span v-n8n-html="i18n.baseText('expressionTip.typeDotPrimitive')" />
</div>

<div v-else-if="tip === 'dotObject'" :class="$style.content">
<span v-html="i18n.baseText('expressionTip.typeDotObject')" />
<span v-n8n-html="i18n.baseText('expressionTip.typeDotObject')" />
</div>

<div v-else :class="$style.content">
<span v-html="i18n.baseText('expressionTip.javascript')" />
<span v-n8n-html="i18n.baseText('expressionTip.javascript')" />
</div>
</div>
</template>
Expand Down
2 changes: 1 addition & 1 deletion packages/editor-ui/src/components/InputPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ export default defineComponent({
<n8n-tooltip v-if="!readOnly" :visible="showDraggableHint && showDraggableHintWithDelay">
<template #content>
<div
v-html="
v-n8n-html="
$locale.baseText('dataMapping.dragFromPreviousHint', {
interpolate: { name: focusedMappableInput },
})
Expand Down
2 changes: 1 addition & 1 deletion packages/editor-ui/src/components/MappingPill.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ withDefaults(defineProps<Props>(), {
<template>
<div
:class="[$style.dragPill, canDrop ? $style.droppablePill : $style.defaultPill]"
v-html="html"
v-n8n-html="html"
/>
</template>

Expand Down
2 changes: 1 addition & 1 deletion packages/editor-ui/src/components/Node.vue
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ function openContextMenu(event: MouseEvent, source: 'node-button' | 'node-right-
<i v-if="isTriggerNode" class="trigger-icon">
<n8n-tooltip placement="bottom">
<template #content>
<span v-html="i18n.baseText('node.thisIsATriggerNode')" />
<span v-n8n-html="i18n.baseText('node.thisIsATriggerNode')" />
</template>
<FontAwesomeIcon icon="bolt" size="lg" />
</n8n-tooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ function onCommunityNodeTooltipClick(event: MouseEvent) {
<p
:class="$style.communityNodeIcon"
@click="onCommunityNodeTooltipClick"
v-html="
v-n8n-html="
i18n.baseText('generic.communityNode.tooltip', {
interpolate: {
packageName: nodeType.name.split('.')[0],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ onMounted(() => {
data-test-id="actions-panel-no-triggers-callout"
>
<span
v-html="
v-n8n-html="
i18n.baseText('nodeCreator.actionsCallout.noTriggerItems', {
interpolate: { nodeName: subcategory ?? '' },
})
Expand All @@ -271,7 +271,7 @@ onMounted(() => {
<p
:class="$style.resetSearch"
@click="resetSearch"
v-html="i18n.baseText('nodeCreator.actionsCategory.noMatchingTriggers')"
v-n8n-html="i18n.baseText('nodeCreator.actionsCategory.noMatchingTriggers')"
/>
</template>
</CategorizedItemsRenderer>
Expand All @@ -293,13 +293,13 @@ onMounted(() => {
slim
data-test-id="actions-panel-activation-callout"
>
<span v-html="i18n.baseText('nodeCreator.actionsCallout.triggersStartWorkflow')" />
<span v-n8n-html="i18n.baseText('nodeCreator.actionsCallout.triggersStartWorkflow')" />
</n8n-callout>
<!-- Empty state -->
<template #empty>
<n8n-info-tip v-if="!search" theme="info" type="note" :class="$style.actionsEmpty">
<span
v-html="
v-n8n-html="
i18n.baseText('nodeCreator.actionsCallout.noActionItems', {
interpolate: { nodeName: subcategory ?? '' },
})
Expand All @@ -311,7 +311,7 @@ onMounted(() => {
:class="$style.resetSearch"
data-test-id="actions-panel-no-matching-actions"
@click="resetSearch"
v-html="i18n.baseText('nodeCreator.actionsCategory.noMatchingActions')"
v-n8n-html="i18n.baseText('nodeCreator.actionsCategory.noMatchingActions')"
/>
</template>
</CategorizedItemsRenderer>
Expand All @@ -320,7 +320,7 @@ onMounted(() => {
<div v-if="containsAPIAction" :class="$style.apiHint">
<span
@click.prevent="addHttpNode"
v-html="
v-n8n-html="
i18n.baseText('nodeCreator.actionsList.apiCall', {
interpolate: { node: subcategory ?? '' },
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ registerKeyHook(`CategoryLeft_${props.category}`, {
<n8n-tooltip placement="top" :popper-class="$style.tooltipPopper">
<n8n-icon icon="question-circle" size="small" />
<template #content>
<div v-html="mouseOverTooltip" />
<div v-n8n-html="mouseOverTooltip" />
</template>
</n8n-tooltip>
</span>
Expand Down
4 changes: 2 additions & 2 deletions packages/editor-ui/src/components/ParameterInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1391,7 +1391,7 @@ onUpdated(async () => {
<div
v-if="option.description"
class="option-description"
v-html="getOptionsOptionDescription(option)"
v-n8n-html="getOptionsOptionDescription(option)"
></div>
</div>
</n8n-option>
Expand Down Expand Up @@ -1424,7 +1424,7 @@ onUpdated(async () => {
<div
v-if="option.description"
class="option-description"
v-html="getOptionsOptionDescription(option)"
v-n8n-html="getOptionsOptionDescription(option)"
></div>
</div>
</n8n-option>
Expand Down
4 changes: 2 additions & 2 deletions packages/editor-ui/src/components/ParameterInputHint.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ const simplyText = computed(() => {
[$style.highlight]: highlight,
}"
>
<span data-test-id="parameter-input-hint" v-html="simplyText"></span>
<span data-test-id="parameter-input-hint" v-n8n-html="simplyText"></span>
</div>
<div
v-else
ref="hintTextRef"
:class="{ [$style.singleline]: singleLine, [$style.highlight]: highlight }"
v-html="sanitizeHtml(hint)"
v-n8n-html="sanitizeHtml(hint)"
></div>
</n8n-text>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default defineComponent({
<div v-if="!rootStore.pushConnectionActive" class="push-connection-lost primary-color">
<n8n-tooltip placement="bottom-end">
<template #content>
<div v-html="$locale.baseText('pushConnectionTracker.cannotConnectToServer')"></div>
<div v-n8n-html="$locale.baseText('pushConnectionTracker.cannotConnectToServer')"></div>
</template>
<span>
<font-awesome-icon icon="exclamation-triangle" />&nbsp;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ defineExpose({
<div class="option-headline">
{{ option.name }}
</div>
<div class="option-description" v-html="option.description" />
<div class="option-description" v-n8n-html="option.description" />
</div>
</n8n-option>
</n8n-select>
Expand Down
Loading

0 comments on commit c93e33c

Please sign in to comment.