Commit 6b85debb authored by Sam Beckham's avatar Sam Beckham Committed by Mike Greiling

Swaps sanitize-html for dompurify

The functionality should be largely the same, though DOMPurify is the
library that's been recomended by our security experts
Co-authored-by: default avatarDheeraj Joshi <djoshi@gitlab.com>
Co-authored-by: default avatarLukas Eipert <leipert@gitlab.com>
parent ef93e2d2
import { take } from 'lodash';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import sanitize from 'sanitize-html';
import { sanitize } from 'dompurify';
import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize());
......@@ -52,7 +52,7 @@ export const sanitizeItem = item => {
return {};
}
return { [key]: sanitize(item[key].toString(), { allowedTags: [] }) };
return { [key]: sanitize(item[key].toString(), { ALLOWED_TAGS: [] }) };
};
return {
......
import sanitize from 'sanitize-html';
import { sanitize } from 'dompurify';
export const parseIssuableData = () => {
try {
......
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import sanitize from 'sanitize-html';
import { sanitize } from 'dompurify';
/**
* Wraps substring matches with HTML `<span>` elements.
......@@ -24,7 +24,7 @@ export default function highlight(string, match = '', matchPrefix = '<b>', match
return string;
}
const sanitizedValue = sanitize(string.toString(), { allowedTags: [] });
const sanitizedValue = sanitize(string.toString(), { ALLOWED_TAGS: [] });
// occurrences is an array of character indices that should be
// highlighted in the original string, i.e. [3, 4, 5, 7]
......
<script>
import marked from 'marked';
import sanitize from 'sanitize-html';
import { sanitize } from 'dompurify';
import katex from 'katex';
import Prompt from './prompt.vue';
......@@ -104,65 +104,58 @@ export default {
return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), {
// allowedTags from GitLab's inline HTML guidelines
// https://docs.gitlab.com/ee/user/markdown.html#inline-html
allowedTags: [
ALLOWED_TAGS: [
'a',
'abbr',
'b',
'blockquote',
'br',
'code',
'dd',
'del',
'div',
'dl',
'dt',
'em',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'h7',
'h8',
'br',
'b',
'hr',
'i',
'strong',
'em',
'a',
'pre',
'code',
'img',
'tt',
'div',
'ins',
'del',
'sup',
'sub',
'p',
'ol',
'ul',
'table',
'thead',
'tbody',
'tfoot',
'blockquote',
'dl',
'dt',
'dd',
'kbd',
'li',
'ol',
'p',
'pre',
'q',
'samp',
'var',
'hr',
'ruby',
'rt',
'rp',
'li',
'tr',
'td',
'th',
'rt',
'ruby',
's',
'strike',
'samp',
'span',
'abbr',
'abbr',
'strike',
'strong',
'sub',
'summary',
'sup',
'table',
'tbody',
'td',
'tfoot',
'th',
'thead',
'tr',
'tt',
'ul',
'var',
],
allowedAttributes: {
'*': ['class', 'style'],
a: ['href'],
img: ['src'],
},
ALLOWED_ATTR: ['class', 'style', 'href', 'src'],
});
},
},
......
<script>
import sanitize from 'sanitize-html';
import { sanitize } from 'dompurify';
import Prompt from '../prompt.vue';
export default {
......@@ -23,10 +23,7 @@ export default {
computed: {
sanitizedOutput() {
return sanitize(this.rawCode, {
allowedTags: sanitize.defaults.allowedTags.concat(['img', 'svg']),
allowedAttributes: {
img: ['src'],
},
ALLOWED_ATTR: ['src'],
});
},
showOutput() {
......
......@@ -2,7 +2,7 @@
import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import sanitize from 'sanitize-html';
import { sanitize } from 'dompurify';
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
......
import Vue from 'vue';
import sanitize from 'sanitize-html';
import { sanitize } from 'dompurify';
import UsersCache from './lib/utils/users_cache';
import UserPopover from './vue_shared/components/user_popover/user_popover.vue';
......
......@@ -639,3 +639,10 @@
:why: MIT license
:versions: []
:when: 2020-07-28 20:35:27.574875000 Z
- - :license
- dompurify
- Apache-2.0
- :who: Lukas Eipert
:why: "https://github.com/cure53/DOMPurify/blob/main/LICENSE and https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31928#note_346604841"
:versions: []
:when: 2020-08-13 13:42:46.508082000 Z
......@@ -40,7 +40,7 @@ module.exports = {
'select2',
'moment-mini',
'aws-sdk',
'sanitize-html',
'dompurify',
'bootstrap/dist/js/bootstrap.js',
'sortablejs/modular/sortable.esm.js',
'popper.js',
......
<script>
import sanitize from 'sanitize-html';
import { sanitize } from 'dompurify';
const ALLOWED_TAGS = ['strong'];
......@@ -12,7 +12,7 @@ export default {
},
computed: {
sanitizedHtml() {
return sanitize(this.html, { allowedTags: ALLOWED_TAGS });
return sanitize(this.html, { ALLOWED_TAGS });
},
},
};
......
export default [
[
'protocol-based JS injection: simple, no spaces',
{
input: `<a href="javascript:alert('XSS');">foo</a>`,
output: '<a>foo</a>',
},
],
[
'protocol-based JS injection: simple, spaces before',
{
input: `<a href="javascript :alert('XSS');">foo</a>`,
output: '<a>foo</a>',
},
],
[
'protocol-based JS injection: simple, spaces after',
{
input: `<a href="javascript: alert('XSS');">foo</a>`,
output: '<a>foo</a>',
},
],
[
'protocol-based JS injection: simple, spaces before and after',
{
input: `<a href="javascript : alert('XSS');">foo</a>`,
output: '<a>foo</a>',
},
],
[
'protocol-based JS injection: preceding colon',
{
input: `<a href=":javascript:alert('XSS');">foo</a>`,
output: '<a>foo</a>',
},
],
[
'protocol-based JS injection: UTF-8 encoding',
{
input: '<a href="javascript&#58;">foo</a>',
output: '<a>foo</a>',
},
],
[
'protocol-based JS injection: long UTF-8 encoding',
{
input: '<a href="javascript&#0058;">foo</a>',
output: '<a>foo</a>',
},
],
[
'protocol-based JS injection: long UTF-8 encoding without semicolons',
{
input:
'<a href=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>foo</a>',
output: '<a>foo</a>',
},
],
[
'protocol-based JS injection: hex encoding',
{
input: '<a href="javascript&#x3A;">foo</a>',
output: '<a>foo</a>',
},
],
[
'protocol-based JS injection: long hex encoding',
{
input: '<a href="javascript&#x003A;">foo</a>',
output: '<a>foo</a>',
},
],
[
'protocol-based JS injection: hex encoding without semicolons',
{
input:
'<a href=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>foo</a>',
output: '<a>foo</a>',
},
],
[
'protocol-based JS injection: null char',
{
input: '<a href=java\u0000script:alert("XSS")>foo</a>',
output: '<a>foo</a>',
},
],
[
'protocol-based JS injection: invalid URL char',
{ input: '<img src=javascript:alert("XSS")>', output: '<img>' },
],
[
'protocol-based JS injection: Unicode',
{
input: `<a href="\u0001java\u0003script:alert('XSS')">foo</a>`,
output: '<a>foo</a>',
},
],
[
'protocol-based JS injection: spaces and entities',
{
input: `<a href=" &#14; javascript:alert('XSS');">foo</a>`,
output: '<a>foo</a>',
},
],
[
'img on error',
{
input: '<img src="x" onerror="alert(document.domain)" />',
output: '<img src="x">',
},
],
['style tags are removed', { input: '<style>.foo {}</style> Foo', output: 'Foo' }],
];
export default {
'protocol-based JS injection: simple, no spaces': {
input: '<a href="javascript:alert(\'XSS\');">foo</a>',
output: '<a>foo</a>',
},
'protocol-based JS injection: simple, spaces before': {
input: '<a href="javascript :alert(\'XSS\');">foo</a>',
output: '<a>foo</a>',
},
'protocol-based JS injection: simple, spaces after': {
input: '<a href="javascript: alert(\'XSS\');">foo</a>',
output: '<a>foo</a>',
},
'protocol-based JS injection: simple, spaces before and after': {
input: '<a href="javascript : alert(\'XSS\');">foo</a>',
output: '<a>foo</a>',
},
'protocol-based JS injection: preceding colon': {
input: '<a href=":javascript:alert(\'XSS\');">foo</a>',
output: '<a>foo</a>',
},
'protocol-based JS injection: UTF-8 encoding': {
input: '<a href="javascript&#58;">foo</a>',
output: '<a>foo</a>',
},
'protocol-based JS injection: long UTF-8 encoding': {
input: '<a href="javascript&#0058;">foo</a>',
output: '<a>foo</a>',
},
'protocol-based JS injection: long UTF-8 encoding without semicolons': {
input:
'<a href=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>foo</a>',
output: '<a>foo</a>',
},
'protocol-based JS injection: hex encoding': {
input: '<a href="javascript&#x3A;">foo</a>',
output: '<a>foo</a>',
},
'protocol-based JS injection: long hex encoding': {
input: '<a href="javascript&#x003A;">foo</a>',
output: '<a>foo</a>',
},
'protocol-based JS injection: hex encoding without semicolons': {
input:
'<a href=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>foo</a>',
output: '<a>foo</a>',
},
'protocol-based JS injection: null char': {
input: '<a href=java\0script:alert("XSS")>foo</a>',
output: '<a>foo</a>',
},
'protocol-based JS injection: invalid URL char': {
input: '<img src=javascript:alert("XSS")>',
output: '<img>',
},
'protocol-based JS injection: Unicode': {
input: '<a href="\u0001java\u0003script:alert(\'XSS\')">foo</a>',
output: '<a>foo</a>',
},
'protocol-based JS injection: spaces and entities': {
input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
output: '<a>foo</a>',
},
'img on error': {
input: '<img src="x" onerror="alert(document.domain)" />',
output: '<img src="x">',
},
};
import Vue from 'vue';
import htmlOutput from '~/notebook/cells/output/html.vue';
import sanitizeTests from './html_sanitize_tests';
import sanitizeTests from './html_sanitize_fixtures';
describe('html output cell', () => {
function createComponent(rawCode) {
......@@ -15,17 +15,12 @@ describe('html output cell', () => {
}).$mount();
}
describe('sanitizes output', () => {
Object.keys(sanitizeTests).forEach(key => {
it(key, () => {
const test = sanitizeTests[key];
const vm = createComponent(test.input);
const outputEl = [...vm.$el.querySelectorAll('div')].pop();
it.each(sanitizeTests)('sanitizes output for: %p', (name, { input, output }) => {
const vm = createComponent(input);
const outputEl = [...vm.$el.querySelectorAll('div')].pop();
expect(outputEl.innerHTML).toEqual(test.output);
expect(outputEl.innerHTML).toEqual(output);
vm.$destroy();
});
});
vm.$destroy();
});
});
......@@ -34,7 +34,7 @@ describe('Output component', () => {
expect(vm.$el.querySelector('pre')).not.toBeNull();
});
it('renders promot', () => {
it('renders prompt', () => {
expect(vm.$el.querySelector('.prompt span')).not.toBeNull();
});
});
......
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { TEST_HOST } from 'helpers/test_constants';
import sanitize from 'sanitize-html';
import { sanitize } from 'dompurify';
import ProjectFindFile from '~/project_find_file';
import axios from '~/lib/utils/axios_utils';
jest.mock('sanitize-html', () => jest.fn(val => val));
jest.mock('dompurify', () => ({
sanitize: jest.fn(val => val),
}));
const BLOB_URL_TEMPLATE = `${TEST_HOST}/namespace/project/blob/master`;
const FILE_FIND_URL = `${TEST_HOST}/namespace/project/files/master?format=json`;
......
......@@ -4150,6 +4150,11 @@ domhandler@^3.0.0:
dependencies:
domelementtype "^2.0.1"
dompurify@^2.0.11:
version "2.0.11"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.0.11.tgz#cd47935774230c5e478b183a572e726300b3891d"
integrity sha512-qVoGPjIW9IqxRij7klDQQ2j6nSe4UNWANBhZNLnsS7ScTtLb+3YdxkRY8brNTpkUiTtcXsCJO+jS0UCDfenLuA==
domutils@^1.5.1:
version "1.6.2"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff"
......
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