import { sanitize, defaultConfig } 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 = defaultConfig.FORBID_ATTR;
const acceptedDataAttrs = ['data-random', 'data-custom'];
describe('~/lib/dompurify', () => {
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('');
});
it("doesn't allow form tags", () => {
expect(sanitize('')).toBe('');
});
describe.each`
type | gon
${'root'} | ${rootGon}
${'absolute'} | ${absoluteGon}
`('when gon contains $type icon urls', ({ type, gon }) => {
beforeEach(() => {
window.gon = gon;
});
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(() => {
window.gon = {};
});
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);
});
});
});