Commit 149846e1 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '247207-missing-icons-dompurify' into 'master'

Have dompurify accept <use> with icons href

Closes #247207

See merge request gitlab-org/gitlab!42665
parents 72a2f162 c17cbfdd
import { take } from 'lodash';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize());
......
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import updateDescription from '../utils/update_description';
......
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
// We currently load + parse the data from the issue app and related merge request
let cachedParsedData;
......
import { sanitize as dompurifySanitize, addHook } from 'dompurify';
import { getBaseURL, relativePathToAbsolute } from '~/lib/utils/url_utility';
// Safely allow SVG <use> tags
const defaultConfig = {
ADD_TAGS: ['use'],
};
// Only icons urls from `gon` are allowed
const getAllowedIconUrls = (gon = window.gon) =>
[gon.sprite_file_icons, gon.sprite_icons].filter(Boolean);
const isUrlAllowed = url => getAllowedIconUrls().some(allowedUrl => url.startsWith(allowedUrl));
const isHrefSafe = url =>
isUrlAllowed(url) || isUrlAllowed(relativePathToAbsolute(url, getBaseURL()));
const removeUnsafeHref = (node, attr) => {
if (!node.hasAttribute(attr)) {
return;
}
if (!isHrefSafe(node.getAttribute(attr))) {
node.removeAttribute(attr);
}
};
/**
* Sanitize icons' <use> tag attributes, to safely include
* svgs such as in:
*
* <svg viewBox="0 0 100 100">
* <use href="/assets/icons-xxx.svg#icon_name"></use>
* </svg>
*
* @param {Object} node - Node to sanitize
*/
const sanitizeSvgIcon = node => {
removeUnsafeHref(node, 'href');
// Note: `xlink:href` is deprecated, but still in use
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
removeUnsafeHref(node, 'xlink:href');
};
addHook('afterSanitizeAttributes', node => {
if (node.tagName.toLowerCase() === 'use') {
sanitizeSvgIcon(node);
}
});
export const sanitize = (val, config = defaultConfig) => dompurifySanitize(val, config);
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
/**
* Wraps substring matches with HTML `<span>` elements.
......
<script>
/* eslint-disable vue/no-v-html */
import marked from 'marked';
import { sanitize } from 'dompurify';
import katex from 'katex';
import { sanitize } from '~/lib/dompurify';
import Prompt from './prompt.vue';
const renderer = new marked.Renderer();
......
<script>
/* eslint-disable vue/no-v-html */
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import Prompt from '../prompt.vue';
export default {
......
......@@ -2,7 +2,7 @@
import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { deprecatedCreateFlash as flash } from '~/flash';
......
import Vue from 'vue';
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import UsersCache from './lib/utils/users_cache';
import UserPopover from './vue_shared/components/user_popover/user_popover.vue';
......
......@@ -432,7 +432,7 @@ To avoid this error, use the applicable HTML entity code (`&lt;` or `&gt;`) inst
- In JavaScript:
```javascript
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
const i18n = { LESS_THAN_ONE_HOUR: sanitize(__('In &lt; 1 hour'), { ALLOWED_TAGS: [] }) };
......
<script>
import { escape } from 'lodash';
import { mapState } from 'vuex';
import { sanitize } from 'dompurify';
import { GlTable, GlLink, GlIcon, GlAvatarLink, GlAvatar, GlTooltipDirective } from '@gitlab/ui';
import { sanitize } from '~/lib/dompurify';
import { __, sprintf, n__ } from '~/locale';
import { getTimeago } from '~/lib/utils/datetime_utility';
import ApproversColumn from './approvers_column.vue';
......
<script>
/* eslint-disable vue/no-v-html */
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
const ALLOWED_TAGS = ['strong'];
......
<script>
import { sanitize } from 'dompurify';
import { GlFormTextarea, GlButton } from '@gitlab/ui';
import { sanitize } from '~/lib/dompurify';
export default {
components: { GlFormTextarea, GlButton },
......
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 = '<svg><use></use></svg>';
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',
'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}`,
];
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('<br>', { ALLOWED_TAGS: [] })).toBe('');
expect(sanitize('<strong></strong>', { ALLOWED_TAGS: [] })).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 = `<svg><use></use></svg>`;
expect(sanitize(htmlHref)).toBe(htmlHref);
});
it.each(safeUrls[type])('allows safe URL %s', url => {
const htmlHref = `<svg><use href="${url}"></use></svg>`;
expect(sanitize(htmlHref)).toBe(htmlHref);
const htmlXlink = `<svg><use xlink:href="${url}"></use></svg>`;
expect(sanitize(htmlXlink)).toBe(htmlXlink);
});
it.each(unsafeUrls)('sanitizes unsafe URL %s', url => {
const htmlHref = `<svg><use href="${url}"></use></svg>`;
const htmlXlink = `<svg><use xlink:href="${url}"></use></svg>`;
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 = `<svg><use href="${url}"></use></svg>`;
const htmlXlink = `<svg><use xlink:href="${url}"></use></svg>`;
expect(sanitize(htmlHref)).toBe(expectedSanitized);
expect(sanitize(htmlXlink)).toBe(expectedSanitized);
});
});
});
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { TEST_HOST } from 'helpers/test_constants';
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import ProjectFindFile from '~/project_find_file';
import axios from '~/lib/utils/axios_utils';
jest.mock('dompurify', () => ({
jest.mock('~/lib/dompurify', () => ({
addHook: jest.fn(),
sanitize: jest.fn(val => val),
}));
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment