diff options
Diffstat (limited to 'spec/frontend/vue_shared/directives/safe_html_spec.js')
-rw-r--r-- | spec/frontend/vue_shared/directives/safe_html_spec.js | 116 |
1 files changed, 116 insertions, 0 deletions
diff --git a/spec/frontend/vue_shared/directives/safe_html_spec.js b/spec/frontend/vue_shared/directives/safe_html_spec.js new file mode 100644 index 00000000000..ba1de8e4596 --- /dev/null +++ b/spec/frontend/vue_shared/directives/safe_html_spec.js @@ -0,0 +1,116 @@ +import { shallowMount } from '@vue/test-utils'; +import safeHtml from '~/vue_shared/directives/safe_html'; +import { defaultConfig } from '~/lib/dompurify'; +/* eslint-disable no-script-url */ +const invalidProtocolUrls = [ + 'javascript:alert(1)', + 'jAvascript:alert(1)', + 'data:text/html,<script>alert(1);</script>', + ' javascript:', + 'javascript :', +]; +/* eslint-enable no-script-url */ +const validProtocolUrls = ['slack://open', 'x-devonthink-item://90909', 'x-devonthink-item:90909']; + +describe('safe html directive', () => { + let wrapper; + + const createComponent = ({ template, html, config } = {}) => { + const defaultTemplate = `<div v-safe-html="rawHtml"></div>`; + const defaultHtml = 'hello <script>alert(1)</script>world'; + + const component = { + directives: { + safeHtml, + }, + data() { + return { + rawHtml: html || defaultHtml, + config: config || {}, + }; + }, + template: template || defaultTemplate, + }; + + wrapper = shallowMount(component); + }; + + describe('default', () => { + it('should remove the script tag', () => { + createComponent(); + + expect(wrapper.html()).toEqual('<div>hello world</div>'); + }); + + it('should remove javascript hrefs', () => { + createComponent({ html: '<a href="javascript:prompt(1)">click here</a>' }); + + expect(wrapper.html()).toEqual('<div><a>click here</a></div>'); + }); + + it('should remove any existing children', () => { + createComponent({ + template: `<div v-safe-html="rawHtml">foo <i>bar</i></div>`, + }); + + expect(wrapper.html()).toEqual('<div>hello world</div>'); + }); + + describe('with non-http links', () => { + it.each(validProtocolUrls)('should allow %s', (url) => { + createComponent({ + html: `<a href="${url}">internal link</a>`, + }); + expect(wrapper.html()).toContain(`<a href="${url}">internal link</a>`); + }); + + it.each(invalidProtocolUrls)('should not allow %s', (url) => { + createComponent({ + html: `<a href="${url}">internal link</a>`, + }); + expect(wrapper.html()).toContain(`<a>internal link</a>`); + }); + }); + + describe('handles data attributes correctly', () => { + const allowedDataAttrs = ['data-safe', 'data-random']; + + it.each(defaultConfig.FORBID_ATTR)('removes dangerous `%s` attribute', (attr) => { + const html = `<a ${attr}="true"></a>`; + createComponent({ html }); + + expect(wrapper.html()).not.toContain(html); + }); + + it.each(allowedDataAttrs)('does not remove allowed `%s` attribute', (attr) => { + const html = `<a ${attr}="true"></a>`; + createComponent({ html }); + + expect(wrapper.html()).toContain(html); + }); + }); + }); + + describe('advance config', () => { + const template = '<div v-safe-html:[config]="rawHtml"></div>'; + it('should only allow <b> tags', () => { + createComponent({ + template, + html: '<a href="javascript:prompt(1)"><b>click here</b></a>', + config: { ALLOWED_TAGS: ['b'] }, + }); + + expect(wrapper.html()).toEqual('<div><b>click here</b></div>'); + }); + + it('should strip all html tags', () => { + createComponent({ + template, + html: '<a href="javascript:prompt(1)"><u>click here</u></a>', + config: { ALLOWED_TAGS: [] }, + }); + + expect(wrapper.html()).toEqual('<div>click here</div>'); + }); + }); +}); |