import { sanitize } from '~/lib/dompurify'; // GDK const rootGon = { sprite_file_icons: '/assets/icons-123a.svg', sprite_icons: '/assets/icons-456b.svg', }; // Production const absoluteGon = { sprite_file_icons: `${window.location.protocol}//${window.location.hostname}/assets/icons-123a.svg`, sprite_icons: `${window.location.protocol}//${window.location.hostname}/assets/icons-456b.svg`, }; const expectedSanitized = ''; const safeUrls = { root: Object.values(rootGon).map((url) => `${url}#ellipsis_h`), absolute: Object.values(absoluteGon).map((url) => `${url}#ellipsis_h`), }; const unsafeUrls = [ '/an/evil/url', '../../../evil/url', 'https://evil.url/assets/icons-123a.svg#test', 'https://evil.url/assets/icons-456b.svg', `https://evil.url/${rootGon.sprite_icons}`, `https://evil.url/${rootGon.sprite_file_icons}`, `https://evil.url/${absoluteGon.sprite_icons}`, `https://evil.url/${absoluteGon.sprite_file_icons}`, `${rootGon.sprite_icons}/../evil/path`, `${rootGon.sprite_file_icons}/../../evil/path`, `${absoluteGon.sprite_icons}/../evil/path`, `${absoluteGon.sprite_file_icons}/../../https://evil.url`, ]; /* eslint-disable no-script-url */ const invalidProtocolUrls = [ 'javascript:alert(1)', 'jAvascript:alert(1)', 'data:text/html,', ' javascript:', 'javascript :', ]; /* eslint-enable no-script-url */ const validProtocolUrls = ['slack://open', 'x-devonthink-item://90909', 'x-devonthink-item:90909']; const forbiddenDataAttrs = ['data-remote', 'data-url', 'data-type', 'data-method']; const acceptedDataAttrs = ['data-random', 'data-custom']; describe('~/lib/dompurify', () => { let originalGon; it('uses local configuration when given', () => { // As dompurify uses a "Persistent Configuration", it might // ignore config, this check verifies we respect // https://github.com/cure53/DOMPurify#persistent-configuration expect(sanitize('
', { ALLOWED_TAGS: [] })).toBe(''); expect(sanitize('', { ALLOWED_TAGS: [] })).toBe(''); }); describe('includes default configuration', () => { it('with empty config', () => { const svgIcon = ''; expect(sanitize(svgIcon, {})).toBe(svgIcon); }); it('with valid config', () => { expect(sanitize('', { ALLOWED_TAGS: ['a'] })).toBe( '', ); }); }); it("doesn't sanitize local references", () => { const htmlHref = ``; const htmlXlink = ``; expect(sanitize(htmlHref)).toBe(htmlHref); expect(sanitize(htmlXlink)).toBe(htmlXlink); }); it("doesn't sanitize gl-emoji", () => { expect(sanitize('

💯

')).toBe('

💯

'); }); it("doesn't allow style tags", () => { // removes style tags expect(sanitize('')).toBe(''); expect(sanitize('')).toBe(''); // removes mstyle tag (this can removed later by disallowing math tags) expect(sanitize('')).toBe(''); // removes link tag (this is DOMPurify's default behavior) expect(sanitize('')).toBe(''); }); describe.each` type | gon ${'root'} | ${rootGon} ${'absolute'} | ${absoluteGon} `('when gon contains $type icon urls', ({ type, gon }) => { beforeAll(() => { originalGon = window.gon; window.gon = gon; }); afterAll(() => { window.gon = originalGon; }); it('allows no href attrs', () => { const htmlHref = ``; expect(sanitize(htmlHref)).toBe(htmlHref); }); it.each(safeUrls[type])('allows safe URL %s', (url) => { const htmlHref = ``; expect(sanitize(htmlHref)).toBe(htmlHref); const htmlXlink = ``; expect(sanitize(htmlXlink)).toBe(htmlXlink); }); it.each(unsafeUrls)('sanitizes unsafe URL %s', (url) => { const htmlHref = ``; const htmlXlink = ``; expect(sanitize(htmlHref)).toBe(expectedSanitized); expect(sanitize(htmlXlink)).toBe(expectedSanitized); }); }); describe('when gon does not contain icon urls', () => { beforeAll(() => { originalGon = window.gon; window.gon = {}; }); afterAll(() => { window.gon = originalGon; }); it.each([...safeUrls.root, ...safeUrls.absolute, ...unsafeUrls])('sanitizes URL %s', (url) => { const htmlHref = ``; const htmlXlink = ``; expect(sanitize(htmlHref)).toBe(expectedSanitized); expect(sanitize(htmlXlink)).toBe(expectedSanitized); }); }); describe('handles data attributes correctly', () => { it.each(forbiddenDataAttrs)('removes %s attributes', (attr) => { const htmlHref = `hello`; expect(sanitize(htmlHref)).toBe('hello'); }); it.each(acceptedDataAttrs)('does not remove %s attributes', (attr) => { const attrWithValue = `${attr}="true"`; const htmlHref = `hello`; expect(sanitize(htmlHref)).toBe(`hello`); }); }); describe('with non-http links', () => { it.each(validProtocolUrls)('should allow %s', (url) => { const html = `internal link`; expect(sanitize(html)).toBe(`internal link`); }); it.each(invalidProtocolUrls)('should not allow %s', (url) => { const html = `internal link`; expect(sanitize(html)).toBe(`internal link`); }); }); describe('links with target attribute', () => { const getSanitizedNode = (html) => { return document.createRange().createContextualFragment(sanitize(html)).firstElementChild; }; it('adds secure context', () => { const html = `link`; const el = getSanitizedNode(html); expect(el.getAttribute('target')).toBe('_blank'); expect(el.getAttribute('rel')).toBe('noopener noreferrer'); }); it('adds secure context and merge existing `rel` values', () => { const html = `link`; const el = getSanitizedNode(html); expect(el.getAttribute('target')).toBe('_blank'); expect(el.getAttribute('rel')).toBe('help external noopener noreferrer'); }); it('does not duplicate noopener/noreferrer `rel` values', () => { const html = `link`; const el = getSanitizedNode(html); expect(el.getAttribute('target')).toBe('_blank'); expect(el.getAttribute('rel')).toBe('noreferrer noopener'); }); it('does not update `rel` values when target is not `_blank`', () => { const html = `internal`; const el = getSanitizedNode(html); expect(el.getAttribute('target')).toBe('_self'); expect(el.getAttribute('rel')).toBe('help'); }); it('does not update `rel` values when target attribute is not present', () => { const html = `link`; const el = getSanitizedNode(html); expect(el.hasAttribute('target')).toBe(false); expect(el.hasAttribute('rel')).toBe(false); }); }); });