Commit 8503b1b5 authored by Himanshu Kapoor's avatar Himanshu Kapoor Committed by Paul Slaughter

Do not sanitize tooltip if it does not contain any html

https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43572
parent fb24b25c
import $ from 'jquery';
import '~/commons/bootstrap';
import { parseBoolean } from '~/lib/utils/common_utils';
export default {
bind(el) {
......@@ -9,6 +10,10 @@ export default {
$(el).tooltip({
trigger: 'hover',
delay,
// By default, sanitize is run even if there is no `html` or `template` present
// so let's optimize to only run this when necessary.
// https://github.com/twbs/bootstrap/blob/c5966de27395a407f9a3d20d0eb2ff8e8fb7b564/js/src/tooltip.js#L716
sanitize: parseBoolean(el.dataset.html) || Boolean(el.dataset.template),
});
},
......
import $ from 'jquery';
import { escape } from 'lodash';
import { mount } from '@vue/test-utils';
import tooltip from '~/vue_shared/directives/tooltip';
const DEFAULT_TOOLTIP_TEMPLATE = '<div v-tooltip :title="tooltip"></div>';
const HTML_TOOLTIP_TEMPLATE = '<div v-tooltip data-html="true" :title="tooltip"></div>';
describe('Tooltip directive', () => {
let vm;
let wrapper;
function createTooltipContainer({
template = DEFAULT_TOOLTIP_TEMPLATE,
text = 'some text',
} = {}) {
wrapper = mount(
{
directives: { tooltip },
data: () => ({ tooltip: text }),
template,
},
{ attachToDocument: true },
);
}
async function showTooltip() {
$(wrapper.vm.$el).tooltip('show');
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
}
function findTooltipInnerHtml() {
return document.querySelector('.tooltip-inner').innerHTML;
}
function findTooltipHtml() {
return document.querySelector('.tooltip').innerHTML;
}
afterEach(() => {
if (vm) {
vm.$destroy();
}
wrapper.destroy();
wrapper = null;
});
describe('with a single tooltip', () => {
beforeEach(() => {
const wrapper = mount(
{
directives: {
tooltip,
},
data() {
return {
tooltip: 'some text',
};
},
template: '<div v-tooltip :title="tooltip"></div>',
},
{ attachToDocument: true },
);
vm = wrapper.vm;
});
it('should have tooltip plugin applied', () => {
expect($(vm.$el).data('bs.tooltip')).toBeDefined();
createTooltipContainer();
expect($(wrapper.vm.$el).data('bs.tooltip')).toBeDefined();
});
it('displays the title as tooltip', () => {
$(vm.$el).tooltip('show');
createTooltipContainer();
$(wrapper.vm.$el).tooltip('show');
jest.runOnlyPendingTimers();
const tooltipElement = document.querySelector('.tooltip-inner');
......@@ -44,52 +61,98 @@ describe('Tooltip directive', () => {
expect(tooltipElement.textContent).toContain('some text');
});
it('updates a visible tooltip', () => {
$(vm.$el).tooltip('show');
it.each`
condition | template | sanitize
${'does not contain any html'} | ${DEFAULT_TOOLTIP_TEMPLATE} | ${false}
${'contains html'} | ${HTML_TOOLTIP_TEMPLATE} | ${true}
`('passes sanitize=$sanitize if the tooltip $condition', ({ template, sanitize }) => {
createTooltipContainer({ template });
expect($(wrapper.vm.$el).data('bs.tooltip').config.sanitize).toEqual(sanitize);
});
it('updates a visible tooltip', async () => {
createTooltipContainer();
$(wrapper.vm.$el).tooltip('show');
jest.runOnlyPendingTimers();
const tooltipElement = document.querySelector('.tooltip-inner');
vm.tooltip = 'other text';
wrapper.vm.tooltip = 'other text';
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
expect(tooltipElement.textContent).toContain('other text');
});
describe('tooltip sanitization', () => {
it('reads tooltip content as text if data-html is not passed', async () => {
createTooltipContainer({ text: 'sample text<script>alert("XSS!!")</script>' });
return vm.$nextTick().then(() => {
expect(tooltipElement.textContent).toContain('other text');
await showTooltip();
const result = findTooltipInnerHtml();
expect(result).toEqual('sample text&lt;script&gt;alert("XSS!!")&lt;/script&gt;');
});
it('sanitizes tooltip if data-html is passed', async () => {
createTooltipContainer({
template: HTML_TOOLTIP_TEMPLATE,
text: 'sample text<script>alert("XSS!!")</script>',
});
await showTooltip();
const result = findTooltipInnerHtml();
expect(result).toEqual('sample text');
expect(result).not.toContain('XSS!!');
});
it('sanitizes tooltip if data-template is passed', async () => {
const tooltipTemplate = escape(
'<div class="tooltip" role="tooltip"><div onclick="alert(\'XSS!\')" class="arrow"></div><div class="tooltip-inner"></div></div>',
);
createTooltipContainer({
template: `<div v-tooltip :title="tooltip" data-html="false" data-template="${tooltipTemplate}"></div>`,
});
await showTooltip();
const result = findTooltipHtml();
expect(result).toEqual(
// objectionable element is removed
'<div class="arrow"></div><div class="tooltip-inner">some text</div>',
);
expect(result).not.toContain('XSS!!');
});
});
});
describe('with multiple tooltips', () => {
beforeEach(() => {
const wrapper = mount(
{
directives: {
tooltip,
},
template: `
<div>
<div
v-tooltip
class="js-look-for-tooltip"
title="foo">
</div>
<div
v-tooltip
title="bar">
</div>
createTooltipContainer({
template: `
<div>
<div
v-tooltip
class="js-look-for-tooltip"
title="foo">
</div>
`,
},
{ attachToDocument: true },
);
vm = wrapper.vm;
<div
v-tooltip
title="bar">
</div>
</div>
`,
});
});
it('should have tooltip plugin applied to all instances', () => {
expect(
$(vm.$el)
$(wrapper.vm.$el)
.find('.js-look-for-tooltip')
.data('bs.tooltip'),
).toBeDefined();
......
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