Commit 3e6d49c0 authored by Mike Greiling's avatar Mike Greiling

Merge branch 'dompurify' into 'master'

Swaps sanitize-html for dompurify

See merge request gitlab-org/gitlab!31928
parents d9cf419f 6b85debb
import { take } from 'lodash'; import { take } from 'lodash';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; 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'; import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize()); export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize());
...@@ -52,7 +52,7 @@ export const sanitizeItem = item => { ...@@ -52,7 +52,7 @@ export const sanitizeItem = item => {
return {}; return {};
} }
return { [key]: sanitize(item[key].toString(), { allowedTags: [] }) }; return { [key]: sanitize(item[key].toString(), { ALLOWED_TAGS: [] }) };
}; };
return { return {
......
import sanitize from 'sanitize-html'; import { sanitize } from 'dompurify';
export const parseIssuableData = () => { export const parseIssuableData = () => {
try { try {
......
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import sanitize from 'sanitize-html'; import { sanitize } from 'dompurify';
/** /**
* Wraps substring matches with HTML `<span>` elements. * Wraps substring matches with HTML `<span>` elements.
...@@ -24,7 +24,7 @@ export default function highlight(string, match = '', matchPrefix = '<b>', match ...@@ -24,7 +24,7 @@ export default function highlight(string, match = '', matchPrefix = '<b>', match
return string; 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 // occurrences is an array of character indices that should be
// highlighted in the original string, i.e. [3, 4, 5, 7] // highlighted in the original string, i.e. [3, 4, 5, 7]
......
<script> <script>
import marked from 'marked'; import marked from 'marked';
import sanitize from 'sanitize-html'; import { sanitize } from 'dompurify';
import katex from 'katex'; import katex from 'katex';
import Prompt from './prompt.vue'; import Prompt from './prompt.vue';
...@@ -104,65 +104,58 @@ export default { ...@@ -104,65 +104,58 @@ export default {
return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), { return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), {
// allowedTags from GitLab's inline HTML guidelines // allowedTags from GitLab's inline HTML guidelines
// https://docs.gitlab.com/ee/user/markdown.html#inline-html // 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', 'h1',
'h2', 'h2',
'h3', 'h3',
'h4', 'h4',
'h5', 'h5',
'h6', 'h6',
'h7', 'hr',
'h8',
'br',
'b',
'i', 'i',
'strong',
'em',
'a',
'pre',
'code',
'img', 'img',
'tt',
'div',
'ins', 'ins',
'del',
'sup',
'sub',
'p',
'ol',
'ul',
'table',
'thead',
'tbody',
'tfoot',
'blockquote',
'dl',
'dt',
'dd',
'kbd', 'kbd',
'li',
'ol',
'p',
'pre',
'q', 'q',
'samp',
'var',
'hr',
'ruby',
'rt',
'rp', 'rp',
'li', 'rt',
'tr', 'ruby',
'td',
'th',
's', 's',
'strike', 'samp',
'span', 'span',
'abbr', 'strike',
'abbr', 'strong',
'sub',
'summary', 'summary',
'sup',
'table',
'tbody',
'td',
'tfoot',
'th',
'thead',
'tr',
'tt',
'ul',
'var',
], ],
allowedAttributes: { ALLOWED_ATTR: ['class', 'style', 'href', 'src'],
'*': ['class', 'style'],
a: ['href'],
img: ['src'],
},
}); });
}, },
}, },
......
<script> <script>
import sanitize from 'sanitize-html'; import { sanitize } from 'dompurify';
import Prompt from '../prompt.vue'; import Prompt from '../prompt.vue';
export default { export default {
...@@ -23,10 +23,7 @@ export default { ...@@ -23,10 +23,7 @@ export default {
computed: { computed: {
sanitizedOutput() { sanitizedOutput() {
return sanitize(this.rawCode, { return sanitize(this.rawCode, {
allowedTags: sanitize.defaults.allowedTags.concat(['img', 'svg']), ALLOWED_ATTR: ['src'],
allowedAttributes: {
img: ['src'],
},
}); });
}, },
showOutput() { showOutput() {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import $ from 'jquery'; import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import sanitize from 'sanitize-html'; import { sanitize } from 'dompurify';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import flash from '~/flash'; import flash from '~/flash';
......
import Vue from 'vue'; import Vue from 'vue';
import sanitize from 'sanitize-html'; import { sanitize } from 'dompurify';
import UsersCache from './lib/utils/users_cache'; import UsersCache from './lib/utils/users_cache';
import UserPopover from './vue_shared/components/user_popover/user_popover.vue'; import UserPopover from './vue_shared/components/user_popover/user_popover.vue';
......
...@@ -639,3 +639,10 @@ ...@@ -639,3 +639,10 @@
:why: MIT license :why: MIT license
:versions: [] :versions: []
:when: 2020-07-28 20:35:27.574875000 Z :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 = { ...@@ -40,7 +40,7 @@ module.exports = {
'select2', 'select2',
'moment-mini', 'moment-mini',
'aws-sdk', 'aws-sdk',
'sanitize-html', 'dompurify',
'bootstrap/dist/js/bootstrap.js', 'bootstrap/dist/js/bootstrap.js',
'sortablejs/modular/sortable.esm.js', 'sortablejs/modular/sortable.esm.js',
'popper.js', 'popper.js',
......
<script> <script>
import sanitize from 'sanitize-html'; import { sanitize } from 'dompurify';
const ALLOWED_TAGS = ['strong']; const ALLOWED_TAGS = ['strong'];
...@@ -12,7 +12,7 @@ export default { ...@@ -12,7 +12,7 @@ export default {
}, },
computed: { computed: {
sanitizedHtml() { 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 Vue from 'vue';
import htmlOutput from '~/notebook/cells/output/html.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', () => { describe('html output cell', () => {
function createComponent(rawCode) { function createComponent(rawCode) {
...@@ -15,17 +15,12 @@ describe('html output cell', () => { ...@@ -15,17 +15,12 @@ describe('html output cell', () => {
}).$mount(); }).$mount();
} }
describe('sanitizes output', () => { it.each(sanitizeTests)('sanitizes output for: %p', (name, { input, output }) => {
Object.keys(sanitizeTests).forEach(key => { const vm = createComponent(input);
it(key, () => { const outputEl = [...vm.$el.querySelectorAll('div')].pop();
const test = sanitizeTests[key];
const vm = createComponent(test.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', () => { ...@@ -34,7 +34,7 @@ describe('Output component', () => {
expect(vm.$el.querySelector('pre')).not.toBeNull(); expect(vm.$el.querySelector('pre')).not.toBeNull();
}); });
it('renders promot', () => { it('renders prompt', () => {
expect(vm.$el.querySelector('.prompt span')).not.toBeNull(); expect(vm.$el.querySelector('.prompt span')).not.toBeNull();
}); });
}); });
......
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery'; import $ from 'jquery';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import sanitize from 'sanitize-html'; import { sanitize } from 'dompurify';
import ProjectFindFile from '~/project_find_file'; import ProjectFindFile from '~/project_find_file';
import axios from '~/lib/utils/axios_utils'; 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 BLOB_URL_TEMPLATE = `${TEST_HOST}/namespace/project/blob/master`;
const FILE_FIND_URL = `${TEST_HOST}/namespace/project/files/master?format=json`; const FILE_FIND_URL = `${TEST_HOST}/namespace/project/files/master?format=json`;
......
...@@ -4150,6 +4150,11 @@ domhandler@^3.0.0: ...@@ -4150,6 +4150,11 @@ domhandler@^3.0.0:
dependencies: dependencies:
domelementtype "^2.0.1" 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: domutils@^1.5.1:
version "1.6.2" version "1.6.2"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff" 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