From eb57935b586134f52bc534728e94e6db893c9935 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 16 Nov 2022 11:01:31 +0100 Subject: [PATCH] Client - move input sanitization in utils --- Makefile | 2 +- fittrackee_client/package.json | 3 +- .../Workout/WorkoutDetail/WorkoutNotes.vue | 37 ++++++---- fittrackee_client/src/main.ts | 2 - fittrackee_client/src/utils/inputs.ts | 9 +++ fittrackee_client/src/vue-3-sanitize.d.ts | 1 - .../tests/unit/utils/inputs.spec.ts | 71 +++++++++++++++++++ fittrackee_client/yarn.lock | 16 ++--- 8 files changed, 113 insertions(+), 28 deletions(-) create mode 100644 fittrackee_client/src/utils/inputs.ts delete mode 100644 fittrackee_client/src/vue-3-sanitize.d.ts create mode 100644 fittrackee_client/tests/unit/utils/inputs.spec.ts diff --git a/Makefile b/Makefile index e623b095..abaf07e1 100644 --- a/Makefile +++ b/Makefile @@ -235,7 +235,7 @@ test-python: $(PYTEST) fittrackee --cov-config .coveragerc --cov=fittrackee --cov-report term-missing $(PYTEST_ARGS) test-client: - cd fittrackee_client && $(NPM) test:unit + cd fittrackee_client && $(NPM) test:unit $(MOCHA_ARGS) type-check: echo 'Running mypy...' diff --git a/fittrackee_client/package.json b/fittrackee_client/package.json index ce767f6b..ee3676c5 100644 --- a/fittrackee_client/package.json +++ b/fittrackee_client/package.json @@ -28,8 +28,8 @@ "linkify-html": "^4.0.2", "linkifyjs": "^4.0.2", "register-service-worker": "^1.7.1", + "sanitize-html": "^2.7.3", "vue": "^3.2.43", - "vue-3-sanitize": "^0.1.4", "vue-chart-3": "3.1.1", "vue-fullscreen": "^3.1.1", "vue-i18n": "^9.2.2", @@ -40,6 +40,7 @@ "@intlify/vue-i18n-loader": "^4.2.0", "@types/chai": "^4.3.4", "@types/mocha": "^10.0.0", + "@types/sanitize-html": "^2.6.2", "@typescript-eslint/eslint-plugin": "^5.42.1", "@typescript-eslint/parser": "^5.42.1", "@vue/cli-plugin-babel": "~5.0.8", diff --git a/fittrackee_client/src/components/Workout/WorkoutDetail/WorkoutNotes.vue b/fittrackee_client/src/components/Workout/WorkoutDetail/WorkoutNotes.vue index dd6079e5..4571eec7 100644 --- a/fittrackee_client/src/components/Workout/WorkoutDetail/WorkoutNotes.vue +++ b/fittrackee_client/src/components/Workout/WorkoutDetail/WorkoutNotes.vue @@ -3,31 +3,38 @@ diff --git a/fittrackee_client/src/main.ts b/fittrackee_client/src/main.ts index a56f81ed..f7f15d86 100644 --- a/fittrackee_client/src/main.ts +++ b/fittrackee_client/src/main.ts @@ -14,7 +14,6 @@ import { } from 'chart.js' import ChartDataLabels from 'chartjs-plugin-datalabels' import { createApp } from 'vue' -import Vue3Sanitize from "vue-3-sanitize" import VueFullscreen from 'vue-fullscreen' import './registerServiceWorker' @@ -48,7 +47,6 @@ const app = createApp(App) .use(store) .use(router) .use(VueFullscreen, { name: 'VFullscreen' }) - .use(Vue3Sanitize, { allowedTags: ['a'], disallowedTagsMode: 'escape' }) .directive('click-outside', clickOutsideDirective) customComponents.forEach((component) => { diff --git a/fittrackee_client/src/utils/inputs.ts b/fittrackee_client/src/utils/inputs.ts new file mode 100644 index 00000000..7ff44a16 --- /dev/null +++ b/fittrackee_client/src/utils/inputs.ts @@ -0,0 +1,9 @@ +import linkifyHtml from 'linkify-html' +import sanitizeHtml from 'sanitize-html' + +export const linkifyAndClean = (input: string): string => { + return sanitizeHtml(linkifyHtml(input, { target: '_blank' }), { + allowedTags: ['a'], + disallowedTagsMode: 'escape', + }) +} diff --git a/fittrackee_client/src/vue-3-sanitize.d.ts b/fittrackee_client/src/vue-3-sanitize.d.ts deleted file mode 100644 index b509ce2e..00000000 --- a/fittrackee_client/src/vue-3-sanitize.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'vue-3-sanitize'; \ No newline at end of file diff --git a/fittrackee_client/tests/unit/utils/inputs.spec.ts b/fittrackee_client/tests/unit/utils/inputs.spec.ts new file mode 100644 index 00000000..509915c1 --- /dev/null +++ b/fittrackee_client/tests/unit/utils/inputs.spec.ts @@ -0,0 +1,71 @@ +import { assert } from 'chai' + +import { linkifyAndClean } from '@/utils/inputs' + +describe('linkifyAndClean (clean input remains unchanged)', () => { + const testInputs = [ + 'just a text\nfor "test"', + 'link: example', + 'link: example', + ] + + testInputs.map((testInput) => { + it(`it returns unmodified input: '${testInput}'`, () => { + assert.equal(linkifyAndClean(testInput), testInput) + }) + }) +}) + +describe('linkifyAndClean (URL is linkified)', () => { + it('it returns URL as link with target blank', () => { + assert.equal( + linkifyAndClean('link: http://www.example.com'), + 'link: http://www.example.com' + ) + }) +}) + +describe('linkifyAndClean (input sanitization)', () => { + const testsParams = [ + { + description: 'it escapes "script" tags', + inputString: "", + expectedString: "<script>alert('evil!')</script>", + }, + { + description: 'it escapes nested tags', + inputString: '

test

', + expectedString: '<p><b>test</b></p>', + }, + { + description: 'it escapes single tag', + inputString: '

test', + expectedString: '<p>test</p>', + }, + { + description: 'it removes css classe', + inputString: '

test
', + expectedString: '<div>test</div>', + }, + { + description: 'it removes style attribute', + inputString: '
test
', + expectedString: '<div>test</div>', + }, + { + description: 'it keeps nested HTML link', + inputString: '

example

', + expectedString: + '<p>example</p>', + }, + ] + + testsParams.map((testParams) => { + it(testParams.description, () => { + assert.equal( + linkifyAndClean(testParams.inputString), + testParams.expectedString + ) + }) + }) +}) diff --git a/fittrackee_client/yarn.lock b/fittrackee_client/yarn.lock index 93d40144..3e4f0fcd 100644 --- a/fittrackee_client/yarn.lock +++ b/fittrackee_client/yarn.lock @@ -1433,6 +1433,13 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== +"@types/sanitize-html@^2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.6.2.tgz#9c47960841b9def1e4c9dfebaaab010a3f6e97b9" + integrity sha512-7Lu2zMQnmHHQGKXVvCOhSziQMpa+R2hMHFefzbYoYMHeaXR0uXqNeOc3JeQQQ8/6Xa2Br/P1IQTLzV09xxAiUQ== + dependencies: + htmlparser2 "^6.0.0" + "@types/semver@^7.3.12": version "7.3.13" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" @@ -6760,7 +6767,7 @@ safe-regex-test@^1.0.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sanitize-html@^2.0.0: +sanitize-html@^2.7.3: version "2.7.3" resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.7.3.tgz#166c868444ee4f9fd7352ac8c63fa86c343fc2bd" integrity sha512-jMaHG29ak4miiJ8wgqA1849iInqORgNv7SLfSw9LtfOhEUQ1C0YHKH73R+hgyufBW9ZFeJrb057k9hjlfBCVlw== @@ -7681,13 +7688,6 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -vue-3-sanitize@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/vue-3-sanitize/-/vue-3-sanitize-0.1.4.tgz#1bb23279e9dd6e39e2c9d992ebb366da3e6f49a2" - integrity sha512-8+iU+MC94ibyOBUpn4g2hk8Dy32M7PKIi3UoUG+6Pd47J/uHIDdOdeXEEmCM/rVdyFqNXqLfO+yZT2BesGNlXA== - dependencies: - sanitize-html "^2.0.0" - vue-chart-3@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/vue-chart-3/-/vue-chart-3-3.1.1.tgz#7498c580444a92513bbea4f5df7803d295c20c9b"