Commit 54f7ef52 authored by jboyson's avatar jboyson

Update spec and fixtures

This updates the spec and fixtures to more accurately
reflect what this component is doing.

I have removed most of the fixtures as they were
effectively just asserrting that dompurify "works"

I have added in tests that pertain to the bugs this
mr is addressing. Specifically that SVGs were not
rendering.
parent 3a77ae6a
<script> <script>
/* eslint-disable vue/no-v-html */ import { GlSafeHtmlDirective } from '@gitlab/ui';
import { sanitize } from '~/lib/dompurify'; import { sanitize } from '~/lib/dompurify';
import Prompt from '../prompt.vue'; import Prompt from '../prompt.vue';
...@@ -7,6 +7,9 @@ export default { ...@@ -7,6 +7,9 @@ export default {
components: { components: {
Prompt, Prompt,
}, },
directives: {
SafeHtml: GlSafeHtmlDirective,
},
props: { props: {
count: { count: {
type: Number, type: Number,
...@@ -20,11 +23,6 @@ export default { ...@@ -20,11 +23,6 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
codeCssClass: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
sanitizedOutput() { sanitizedOutput() {
...@@ -40,7 +38,6 @@ export default { ...@@ -40,7 +38,6 @@ export default {
<template> <template>
<div class="output"> <div class="output">
<prompt type="Out" :count="count" :show-output="showOutput" /> <prompt type="Out" :count="count" :show-output="showOutput" />
<div v-if="sanitizedOutput.length" class="gl-overflow-auto" v-html="sanitizedOutput"></div> <div class="gl-overflow-auto" v-safe-html="sanitizedOutput"></div>
<pre v-else ref="code" :class="codeCssClass" class="language-python" v-text="rawCode"></pre>
</div> </div>
</template> </template>
/**
* Jupyter notebooks handles the following data types
* that are to be handled by `html.vue`
*
* 'text/html';
* 'image/svg+xml';
*
* This file sets up fixtures for each of these types
* NOTE: The inputs are taken directly from data derived from the
* jupyter notebook file used to test nbview here:
* https://nbviewer.jupyter.org/github/ipython/ipython-in-depth/blob/master/examples/IPython%20Kernel/Rich%20Output.ipynb
*/
export default [ export default [
[ [
'protocol-based JS injection: simple, no spaces', 'text/html table',
{ {
input: `<a href="javascript:alert('XSS');">foo</a>`, input: [
output: '<a>foo</a>', '<table>\n',
'<tr>\n',
'<th>Header 1</th>\n',
'<th>Header 2</th>\n',
'</tr>\n',
'<tr>\n',
'<td>row 1, cell 1</td>\n',
'<td>row 1, cell 2</td>\n',
'</tr>\n',
'<tr>\n',
'<td>row 2, cell 1</td>\n',
'<td>row 2, cell 2</td>\n',
'</tr>\n',
'</table>',
].join(''),
output: '<table>',
}, },
], ],
// Note: style is sanitized out
[ [
'protocol-based JS injection: simple, spaces before', 'text/html style',
{ {
input: `<a href="javascript :alert('XSS');">foo</a>`, input: [
output: '<a>foo</a>', '<style type="text/css">\n',
'\n',
'circle {\n',
' fill: rgb(31, 119, 180);\n',
' fill-opacity: .25;\n',
' stroke: rgb(31, 119, 180);\n',
' stroke-width: 1px;\n',
'}\n',
'\n',
'.leaf circle {\n',
' fill: #ff7f0e;\n',
' fill-opacity: 1;\n',
'}\n',
'\n',
'text {\n',
' font: 10px sans-serif;\n',
'}\n',
'\n',
'</style>',
].join(''),
output: '<!---->',
}, },
], ],
// Note: iframe is sanitized out
[ [
'protocol-based JS injection: simple, spaces after', 'text/html iframe',
{ {
input: `<a href="javascript: alert('XSS');">foo</a>`, input: [
output: '<a>foo</a>', '\n',
' <iframe\n',
' width="400"\n',
' height="300"\n',
' src="https://www.youtube.com/embed/sjfsUzECqK0"\n',
' frameborder="0"\n',
' allowfullscreen\n',
' ></iframe>\n',
' ',
].join(''),
output: '<!---->',
}, },
], ],
[ [
'protocol-based JS injection: simple, spaces before and after', 'image/svg+xml',
{ {
input: `<a href="javascript : alert('XSS');">foo</a>`, input: [
output: '<a>foo</a>', '<svg height="115.02pt" id="svg2" version="1.0" width="388.84pt" xmlns="http://www.w3.org/2000/svg">\n',
' <g>\n',
' <path d="M 184.61344,61.929363 C 184.61344,47.367213 180.46118,39.891193 172.15666,39.481813" style="fill:#646464;fill-opacity:1"/>\n',
' </g>\n',
'</svg>',
].join(),
output: '<svg height="115.02pt" id="svg2"',
}, },
], ],
[
'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' }],
]; ];
import Vue from 'vue'; import { mount } from '@vue/test-utils';
import htmlOutput from '~/notebook/cells/output/html.vue'; import HtmlOutput from '~/notebook/cells/output/html.vue';
import sanitizeTests from './html_sanitize_fixtures'; import sanitizeTests from './html_sanitize_fixtures';
describe('html output cell', () => { describe('html output cell', () => {
function createComponent(rawCode) { function createComponent(rawCode) {
const Component = Vue.extend(htmlOutput); return mount(HtmlOutput, {
return new Component({
propsData: { propsData: {
rawCode, rawCode,
count: 0, count: 0,
index: 0, index: 0,
}, },
}).$mount(); });
} }
it.each(sanitizeTests)('sanitizes output for: %p', (name, { input, output }) => { it.each(sanitizeTests)('sanitizes output for: %p', (name, { input, output }) => {
const vm = createComponent(input); const vm = createComponent(input);
const outputEl = [...vm.$el.querySelectorAll('div')].pop();
expect(outputEl.innerHTML).toEqual(output);
vm.$destroy(); expect(vm.html()).toContain(output);
}); });
}); });
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