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`, ]; 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('

💯

'); }); 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`); }); }); });