From 4b233089d0bdba560e958980b7a71feda901840d Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Mon, 12 Sep 2022 15:49:38 +0200 Subject: [PATCH 1/5] feat(editor): Implement HTML sanitization when using `dangerouslyUseHTMLString` option of Notification and Message components --- package-lock.json | 88 +++++++++++++++++++ packages/editor-ui/package.json | 2 + .../editor-ui/src/components/PageAlert.vue | 3 +- .../src/components/mixins/showMessage.ts | 9 +- packages/editor-ui/src/utils.ts | 11 +++ 5 files changed, 110 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f861a8a24e000..77b661eb3e5be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52623,6 +52623,7 @@ "luxon": "^2.3.0", "monaco-editor": "^0.30.1", "n8n-design-system": "~0.33.1", + "sanitize-html": "^2.7.1", "timeago.js": "^4.0.2", "v-click-outside": "^3.1.2", "vue-fragment": "1.5.1", @@ -52646,6 +52647,7 @@ "@types/luxon": "^2.0.9", "@types/node": "^16.11.22", "@types/quill": "^2.0.1", + "@types/sanitize-html": "^2.6.2", "@types/uuid": "^8.3.2", "@vue/cli-plugin-babel": "~4.5.19", "@vue/cli-plugin-typescript": "~4.5.19", @@ -52691,6 +52693,55 @@ "vuex": "^3.1.1" } }, + "packages/editor-ui/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "packages/editor-ui/node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "packages/editor-ui/node_modules/postcss": { + "version": "8.4.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", + "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "packages/editor-ui/node_modules/sanitize-html": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.1.tgz", + "integrity": "sha512-oOpe8l4J8CaBk++2haoN5yNI5beekjuHv3JRPKUx/7h40Rdr85pemn4NkvUB3TcBP7yjat574sPlcMAyv4UQig==", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^6.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, "packages/node-dev": { "name": "n8n-node-dev", "version": "0.72.1", @@ -81918,6 +81969,7 @@ "@types/luxon": "^2.0.9", "@types/node": "^16.11.22", "@types/quill": "^2.0.1", + "@types/sanitize-html": "^2.6.2", "@types/uuid": "^8.3.2", "@vue/cli-plugin-babel": "~4.5.19", "@vue/cli-plugin-typescript": "~4.5.19", @@ -81947,6 +81999,7 @@ "prismjs": "^1.17.1", "quill": "^2.0.0-dev.3", "quill-autoformat": "^0.1.1", + "sanitize-html": "^2.7.1", "sass": "^1.26.5", "sass-loader": "^8.0.2", "string-template-parser": "^1.2.6", @@ -81971,6 +82024,41 @@ "vue2-touch-events": "^3.2.1", "vuex": "^3.1.1", "xss": "^1.0.10" + }, + "dependencies": { + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "postcss": { + "version": "8.4.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", + "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "sanitize-html": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.1.tgz", + "integrity": "sha512-oOpe8l4J8CaBk++2haoN5yNI5beekjuHv3JRPKUx/7h40Rdr85pemn4NkvUB3TcBP7yjat574sPlcMAyv4UQig==", + "requires": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^6.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + } } }, "n8n-node-dev": { diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 37f60a7b24bb6..0bdf64ef48751 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -29,6 +29,7 @@ "luxon": "^2.3.0", "monaco-editor": "^0.30.1", "n8n-design-system": "~0.33.1", + "sanitize-html": "^2.7.1", "timeago.js": "^4.0.2", "v-click-outside": "^3.1.2", "vue-fragment": "1.5.1", @@ -52,6 +53,7 @@ "@types/luxon": "^2.0.9", "@types/node": "^16.11.22", "@types/quill": "^2.0.1", + "@types/sanitize-html": "^2.6.2", "@types/uuid": "^8.3.2", "@vue/cli-plugin-babel": "~4.5.19", "@vue/cli-plugin-typescript": "~4.5.19", diff --git a/packages/editor-ui/src/components/PageAlert.vue b/packages/editor-ui/src/components/PageAlert.vue index 1a3df215e8f19..0495e851ca115 100644 --- a/packages/editor-ui/src/components/PageAlert.vue +++ b/packages/editor-ui/src/components/PageAlert.vue @@ -7,6 +7,7 @@ import mixins from 'vue-typed-mixins'; import { showMessage } from './mixins/showMessage'; import { ElMessageComponent } from 'element-ui/types/message'; +import { sanitizeHtml } from '@/utils'; export default mixins( showMessage, @@ -28,7 +29,7 @@ export default mixins( }, mounted() { this.alert = this.$showAlert({ - message: this.message, + message: sanitizeHtml(this.message), type: 'warning', duration: 0, showClose: true, diff --git a/packages/editor-ui/src/components/mixins/showMessage.ts b/packages/editor-ui/src/components/mixins/showMessage.ts index e949329d7cc99..d982f22c735b3 100644 --- a/packages/editor-ui/src/components/mixins/showMessage.ts +++ b/packages/editor-ui/src/components/mixins/showMessage.ts @@ -7,6 +7,7 @@ import { ExecutionError } from 'n8n-workflow'; import { ElMessageBoxOptions } from 'element-ui/types/message-box'; import { ElMessage, ElMessageComponent, ElMessageOptions, MessageType } from 'element-ui/types/message'; import { isChildOf } from './helpers'; +import { sanitizeHtml } from '@/utils'; let stickyNotificationQueue: ElNotificationComponent[] = []; @@ -17,6 +18,8 @@ export const showMessage = mixins(externalHooks).extend({ track = true, ) { messageData.dangerouslyUseHTMLString = true; + messageData.message = messageData.message ? sanitizeHtml(messageData.message) : messageData.message; + if (messageData.position === undefined) { messageData.position = 'bottom-right'; } @@ -159,7 +162,8 @@ export const showMessage = mixins(externalHooks).extend({ ...(type && { type }), }; - await this.$confirm(message, headline, options); + const sanitizedMessage = sanitizeHtml(message); + await this.$confirm(sanitizedMessage, headline, options); return true; } catch (e) { return false; @@ -176,7 +180,8 @@ export const showMessage = mixins(externalHooks).extend({ ...(type && { type }), }; - await this.$confirm(message, headline, options); + const sanitizedMessage = sanitizeHtml(message); + await this.$confirm(sanitizedMessage, headline, options); return 'confirmed'; } catch (e) { return e as string; diff --git a/packages/editor-ui/src/utils.ts b/packages/editor-ui/src/utils.ts index 5b8786c8a9936..aa175dd464a3a 100644 --- a/packages/editor-ui/src/utils.ts +++ b/packages/editor-ui/src/utils.ts @@ -1,3 +1,5 @@ +import sanitizeHtmlModule from 'sanitize-html'; + export const omit = (keyToOmit: string, { [keyToOmit]: _, ...remainder }) => remainder; export function isObjectLiteral(maybeObject: unknown): maybeObject is { [key: string]: string } { @@ -12,3 +14,12 @@ export function isJsonKeyObject(item: unknown): item is { return Object.keys(item).includes('json'); } + +export function sanitizeHtml(dirtyHtml: string) { + return sanitizeHtmlModule(dirtyHtml, { + allowedAttributes: { + '*': ['href','name', 'target', 'data-*', 'title'], + }, + allowedTags: ['p', 'strong', 'b', 'code', 'a', 'br', 'i', 'em', 'small' ], + }); +} From 106a494b3b9048ea15ae95af13be25aa559df85d Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Tue, 13 Sep 2022 10:32:10 +0200 Subject: [PATCH 2/5] :bug: Implement mechanism to allow for A href actions from locale strings --- packages/editor-ui/src/App.vue | 2 + .../components/mixins/globalLinkActions.ts | 47 +++++++++++++++++++ .../src/components/mixins/pushConnection.ts | 15 +++--- .../src/plugins/i18n/locales/en.json | 2 +- packages/editor-ui/src/utils.ts | 2 +- packages/editor-ui/src/views/NodeView.vue | 2 +- 6 files changed, 58 insertions(+), 12 deletions(-) create mode 100644 packages/editor-ui/src/components/mixins/globalLinkActions.ts diff --git a/packages/editor-ui/src/App.vue b/packages/editor-ui/src/App.vue index 7faa2132d766c..f48dcfb2176f7 100644 --- a/packages/editor-ui/src/App.vue +++ b/packages/editor-ui/src/App.vue @@ -30,11 +30,13 @@ import { mapGetters } from 'vuex'; import { userHelpers } from './components/mixins/userHelpers'; import { addHeaders, loadLanguage } from './plugins/i18n'; import { restApi } from '@/components/mixins/restApi'; +import { globalLinkActions } from '@/components/mixins/globalLinkActions'; export default mixins( showMessage, userHelpers, restApi, + globalLinkActions, ).extend({ name: 'App', components: { diff --git a/packages/editor-ui/src/components/mixins/globalLinkActions.ts b/packages/editor-ui/src/components/mixins/globalLinkActions.ts new file mode 100644 index 0000000000000..c1127b86f8349 --- /dev/null +++ b/packages/editor-ui/src/components/mixins/globalLinkActions.ts @@ -0,0 +1,47 @@ +/** + * Creates event listeners for `data-action` attribute to allow for actions to be called from locale without using + * unsafe onclick attribute + */ + import Vue from 'vue'; + + export const globalLinkActions = Vue.extend({ + data(): {[key: string]: {[key: string]: Function}} { + return { + customActions: {}, + }; + }, + mounted() { + window.addEventListener('click', this.delegateClick); + this.$root.$on('registerGlobalLinkAction', this.registerCustomAction); + }, + destroyed() { + window.removeEventListener('click', this.delegateClick); + this.$root.$off('registerGlobalLinkAction', this.registerCustomAction); + }, + computed: { + availableActions(): {[key: string]: Function} { + return { + reload: this.reload, + ...this.customActions, + }; + }, + }, + methods: { + registerCustomAction(key: string, action: Function) { + this.customActions[key] = action; + }, + delegateClick(e: MouseEvent) { + const clickedElement = e.target; + if (!(clickedElement instanceof Element) || clickedElement.tagName !== 'A') return; + + const actionAttribute = clickedElement.getAttribute('data-action'); + if(actionAttribute && typeof this.availableActions[actionAttribute] === 'function') { + this.availableActions[actionAttribute](); + } + }, + reload() { + window.location.reload(); + }, + }, + }); + diff --git a/packages/editor-ui/src/components/mixins/pushConnection.ts b/packages/editor-ui/src/components/mixins/pushConnection.ts index 46be03672c976..ac4c7dad8919a 100644 --- a/packages/editor-ui/src/components/mixins/pushConnection.ts +++ b/packages/editor-ui/src/components/mixins/pushConnection.ts @@ -233,7 +233,12 @@ export const pushConnection = mixins( let action; if (!isSavingExecutions) { - action = 'Turn on saving manual executions and run again to see what happened after this node.'; + this.$root.$emit('registerGlobalLinkAction', 'open-settings', async () => { + if (this.$store.getters.isNewWorkflow) await this.saveAsNewWorkflow(); + this.$store.dispatch('ui/openModal', WORKFLOW_SETTINGS_MODAL_KEY); + }); + + action = 'Turn on saving manual executions and run again to see what happened after this node.'; } else { action = `View the execution to see what happened after this node.`; @@ -246,14 +251,6 @@ export const pushConnection = mixins( message: `${action} More info`, type: 'success', duration: 0, - onLinkClick: async (e: HTMLLinkElement) => { - if (e.classList.contains('open-settings')) { - if (this.$store.getters.isNewWorkflow) { - await this.saveAsNewWorkflow(); - } - this.$store.dispatch('ui/openModal', WORKFLOW_SETTINGS_MODAL_KEY); - } - }, }); } else if (runDataExecuted.finished !== true) { this.$titleSet(workflow.name as string, 'ERROR'); diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index cfcd12eed915a..f33622bd92118 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -836,7 +836,7 @@ "showMessage.ok": "OK", "showMessage.showDetails": "Show Details", "startupError": "Error connecting to n8n", - "startupError.message": "Could not connect to server. Refresh to try again", + "startupError.message": "Could not connect to server. Refresh to try again", "tagsDropdown.createTag": "Create tag \"{filter}\"", "tagsDropdown.manageTags": "Manage tags", "tagsDropdown.noMatchingTagsExist": "No matching tags exist", diff --git a/packages/editor-ui/src/utils.ts b/packages/editor-ui/src/utils.ts index aa175dd464a3a..7b1361c9ab6cd 100644 --- a/packages/editor-ui/src/utils.ts +++ b/packages/editor-ui/src/utils.ts @@ -18,7 +18,7 @@ export function isJsonKeyObject(item: unknown): item is { export function sanitizeHtml(dirtyHtml: string) { return sanitizeHtmlModule(dirtyHtml, { allowedAttributes: { - '*': ['href','name', 'target', 'data-*', 'title'], + '*': ['href','name', 'target', 'data-*', 'title', 'class'], }, allowedTags: ['p', 'strong', 'b', 'code', 'a', 'br', 'i', 'em', 'small' ], }); diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 1eff378543b34..550bc22a576ac 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -634,7 +634,7 @@ export default mixins( if ((data as IExecutionsSummary).waitTill) { this.$showMessage({ title: this.$locale.baseText('nodeView.thisExecutionHasntFinishedYet'), - message: `${this.$locale.baseText('nodeView.refresh')} ${this.$locale.baseText('nodeView.toSeeTheLatestStatus')}.
${this.$locale.baseText('nodeView.moreInfo')}`, + message: `${this.$locale.baseText('nodeView.refresh')} ${this.$locale.baseText('nodeView.toSeeTheLatestStatus')}.
${this.$locale.baseText('nodeView.moreInfo')}`, type: 'warning', duration: 0, }); From d693cdef02df1a3ed0d9367cd342d967a2a2eaf6 Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Tue, 13 Sep 2022 10:33:56 +0200 Subject: [PATCH 3/5] :bug: Prevent link action default --- packages/editor-ui/src/components/mixins/globalLinkActions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/editor-ui/src/components/mixins/globalLinkActions.ts b/packages/editor-ui/src/components/mixins/globalLinkActions.ts index c1127b86f8349..9f0bcdd950e96 100644 --- a/packages/editor-ui/src/components/mixins/globalLinkActions.ts +++ b/packages/editor-ui/src/components/mixins/globalLinkActions.ts @@ -36,6 +36,7 @@ const actionAttribute = clickedElement.getAttribute('data-action'); if(actionAttribute && typeof this.availableActions[actionAttribute] === 'function') { + e.preventDefault(); this.availableActions[actionAttribute](); } }, From b9cedc1e03791acaad144cf5e866f6556dce1da2 Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Tue, 13 Sep 2022 11:51:36 +0200 Subject: [PATCH 4/5] :recycle: Use `xss` library instead of `sanitize-html` to handle sanitization --- package-lock.json | 88 --------------------------------- packages/editor-ui/package.json | 2 - packages/editor-ui/src/utils.ts | 31 ++++++++++-- 3 files changed, 26 insertions(+), 95 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77b661eb3e5be..f861a8a24e000 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52623,7 +52623,6 @@ "luxon": "^2.3.0", "monaco-editor": "^0.30.1", "n8n-design-system": "~0.33.1", - "sanitize-html": "^2.7.1", "timeago.js": "^4.0.2", "v-click-outside": "^3.1.2", "vue-fragment": "1.5.1", @@ -52647,7 +52646,6 @@ "@types/luxon": "^2.0.9", "@types/node": "^16.11.22", "@types/quill": "^2.0.1", - "@types/sanitize-html": "^2.6.2", "@types/uuid": "^8.3.2", "@vue/cli-plugin-babel": "~4.5.19", "@vue/cli-plugin-typescript": "~4.5.19", @@ -52693,55 +52691,6 @@ "vuex": "^3.1.1" } }, - "packages/editor-ui/node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "packages/editor-ui/node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "packages/editor-ui/node_modules/postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - } - ], - "dependencies": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "packages/editor-ui/node_modules/sanitize-html": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.1.tgz", - "integrity": "sha512-oOpe8l4J8CaBk++2haoN5yNI5beekjuHv3JRPKUx/7h40Rdr85pemn4NkvUB3TcBP7yjat574sPlcMAyv4UQig==", - "dependencies": { - "deepmerge": "^4.2.2", - "escape-string-regexp": "^4.0.0", - "htmlparser2": "^6.0.0", - "is-plain-object": "^5.0.0", - "parse-srcset": "^1.0.2", - "postcss": "^8.3.11" - } - }, "packages/node-dev": { "name": "n8n-node-dev", "version": "0.72.1", @@ -81969,7 +81918,6 @@ "@types/luxon": "^2.0.9", "@types/node": "^16.11.22", "@types/quill": "^2.0.1", - "@types/sanitize-html": "^2.6.2", "@types/uuid": "^8.3.2", "@vue/cli-plugin-babel": "~4.5.19", "@vue/cli-plugin-typescript": "~4.5.19", @@ -81999,7 +81947,6 @@ "prismjs": "^1.17.1", "quill": "^2.0.0-dev.3", "quill-autoformat": "^0.1.1", - "sanitize-html": "^2.7.1", "sass": "^1.26.5", "sass-loader": "^8.0.2", "string-template-parser": "^1.2.6", @@ -82024,41 +81971,6 @@ "vue2-touch-events": "^3.2.1", "vuex": "^3.1.1", "xss": "^1.0.10" - }, - "dependencies": { - "is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", - "requires": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "sanitize-html": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.1.tgz", - "integrity": "sha512-oOpe8l4J8CaBk++2haoN5yNI5beekjuHv3JRPKUx/7h40Rdr85pemn4NkvUB3TcBP7yjat574sPlcMAyv4UQig==", - "requires": { - "deepmerge": "^4.2.2", - "escape-string-regexp": "^4.0.0", - "htmlparser2": "^6.0.0", - "is-plain-object": "^5.0.0", - "parse-srcset": "^1.0.2", - "postcss": "^8.3.11" - } - } } }, "n8n-node-dev": { diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 0bdf64ef48751..37f60a7b24bb6 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -29,7 +29,6 @@ "luxon": "^2.3.0", "monaco-editor": "^0.30.1", "n8n-design-system": "~0.33.1", - "sanitize-html": "^2.7.1", "timeago.js": "^4.0.2", "v-click-outside": "^3.1.2", "vue-fragment": "1.5.1", @@ -53,7 +52,6 @@ "@types/luxon": "^2.0.9", "@types/node": "^16.11.22", "@types/quill": "^2.0.1", - "@types/sanitize-html": "^2.6.2", "@types/uuid": "^8.3.2", "@vue/cli-plugin-babel": "~4.5.19", "@vue/cli-plugin-typescript": "~4.5.19", diff --git a/packages/editor-ui/src/utils.ts b/packages/editor-ui/src/utils.ts index 7b1361c9ab6cd..855f101a68d41 100644 --- a/packages/editor-ui/src/utils.ts +++ b/packages/editor-ui/src/utils.ts @@ -1,4 +1,4 @@ -import sanitizeHtmlModule from 'sanitize-html'; +import xss, { friendlyAttrValue } from 'xss'; export const omit = (keyToOmit: string, { [keyToOmit]: _, ...remainder }) => remainder; @@ -16,10 +16,31 @@ export function isJsonKeyObject(item: unknown): item is { } export function sanitizeHtml(dirtyHtml: string) { - return sanitizeHtmlModule(dirtyHtml, { - allowedAttributes: { - '*': ['href','name', 'target', 'data-*', 'title', 'class'], + const allowedAttributes = ['href','name', 'target', 'title', 'class', 'id']; + const allowedTags = ['p', 'strong', 'b', 'code', 'a', 'br', 'i', 'em', 'small' ]; + + const sanitizedHtml = xss(dirtyHtml, { + onTagAttr: (tag, name, value) => { + if (tag === 'img' && name === 'src') { + // Only allow http requests to supported image files from the `static` directory + const isImageFile = value.split('#')[0].match(/\.(jpeg|jpg|gif|png|webp)$/) !== null; + const isStaticImageFile = isImageFile && value.startsWith('/static/'); + if (!value.startsWith('https://') && !isStaticImageFile) { + return ''; + } + } + + // Allow `allowedAttributes` and all `data-*` attributes + if(allowedAttributes.includes(name) || name.startsWith('data-')) return `${name}="${friendlyAttrValue(value)}"`; + + return; + // Return nothing, means keep the default handling measure + }, + onTag: (tag) => { + if(!allowedTags.includes(tag)) return ''; + return; }, - allowedTags: ['p', 'strong', 'b', 'code', 'a', 'br', 'i', 'em', 'small' ], }); + + return sanitizedHtml; } From 29ff733c54ac5c3ebce3065f5975dafbf47b8061 Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Tue, 13 Sep 2022 15:35:06 +0200 Subject: [PATCH 5/5] :fire: Remove `onLinkClick` functionality of `$showMessage` --- .../src/components/mixins/showMessage.ts | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/packages/editor-ui/src/components/mixins/showMessage.ts b/packages/editor-ui/src/components/mixins/showMessage.ts index d982f22c735b3..dff7a6afca426 100644 --- a/packages/editor-ui/src/components/mixins/showMessage.ts +++ b/packages/editor-ui/src/components/mixins/showMessage.ts @@ -50,7 +50,6 @@ export const showMessage = mixins(externalHooks).extend({ duration?: number, customClass?: string, closeOnClick?: boolean, - onLinkClick?: (e: HTMLLinkElement) => void, type?: MessageType, }) { // eslint-disable-next-line prefer-const @@ -67,26 +66,6 @@ export const showMessage = mixins(externalHooks).extend({ }; } - if (config.onLinkClick) { - const onLinkClick = (e: MouseEvent) => { - if (e && e.target && config.onLinkClick && isChildOf(notification.$el, e.target as Element)) { - const target = e.target as HTMLElement; - if (target && target.tagName === 'A') { - config.onLinkClick(e.target as HTMLLinkElement); - } - } - }; - window.addEventListener('click', onLinkClick); - - const cb = config.onClose; - config.onClose = () => { - window.removeEventListener('click', onLinkClick); - if (cb) { - cb(); - } - }; - } - notification = this.$showMessage({ title: config.title, message: config.message,