Skip to content
This repository has been archived by the owner on Apr 6, 2021. It is now read-only.

Commit

Permalink
Disable HTML support
Browse files Browse the repository at this point in the history
* Simplify parsing (no need to deal with HTML malformed blocks anymore)
* Add noreferrer and noopener rels to links

cf. #122
  • Loading branch information
willdurand committed Apr 21, 2016
1 parent 1f7c1a7 commit 8cb7aa6
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 86 deletions.
31 changes: 16 additions & 15 deletions app/components/Preview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import PreviewLoader from './loaders/Preview';
import { Templates } from './TemplateForm';
import grayMatter from 'gray-matter';
import isEqual from 'lodash.isequal';
import sanitizeHtml from 'sanitize-html';


const { array, func, number, object, string } = PropTypes;
Expand Down Expand Up @@ -70,8 +69,8 @@ export default class Preview extends Component {

componentWillMount() {
this.props.previewLoader().then((deps) => {
this.markdownIt = deps.markdownIt({
html: true,
this.markdownIt = deps.markdownIt('commonmark', {
html: false,
linkify: true,
typographer: true,
highlight: (str, lang) => {
Expand All @@ -84,7 +83,19 @@ export default class Preview extends Component {
}

return ''; // use external default escaping
},
modifyToken: function (token, env) {
switch (token.type) {
case 'link_open':
token.attrObj.rel = 'noreferrer noopener';
break;
}
}
})
.enable('linkify');

deps.markdownItPlugins.forEach((plugin) => {
this.markdownIt.use(plugin);
});

this.emojione = deps.emojione;
Expand Down Expand Up @@ -129,19 +140,9 @@ export default class Preview extends Component {
*/
getChunks(raw, env) {
// Parse the whole markdown document and get tokens
let tokens = this.markdownIt.parse(raw, env);

// Sanitize html chunks to avoid browser DOM manipulation
// that could possibly crash the app (because of React)
tokens = tokens.map((token) => {
if (token.type === 'html_block') {
token.content = sanitizeHtml(token.content);
}

return token;
});

const tokens = this.markdownIt.parse(raw, env);
const chunks = [];

let start = 0;
let stop = 0;

Expand Down
164 changes: 94 additions & 70 deletions app/components/__tests__/Preview-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from 'react';
import { mount, shallow, render } from 'enzyme';
import { expect } from 'chai';
import mdit from 'markdown-it';
import mditfa from 'markdown-it-fontawesome';
import mditmt from 'markdown-it-modify-token';
import emojione from 'emojione';
import hljs from 'highlight.js';

Expand All @@ -20,6 +22,10 @@ describe('<Preview />', () => {

return Promise.resolve({
markdownIt: mdit,
markdownItPlugins: [
mditfa,
mditmt,
],
hljs: hljs,
emojione: emojione
});
Expand Down Expand Up @@ -229,76 +235,6 @@ describe('<Preview />', () => {
}, 5);
});


it('handles html block chunks', (done) => {
let chunks;
const wrapper = shallow(
<Preview
raw={''}
pos={0}
previewLoader={previewLoader}
template={''}
/>
);

let html = [
'<div class="foo">',
' <h3>sub-section</h3>',
' <p>lorem ipsum</p>',
'</div>'
];

setTimeout(() => {
const preview = wrapper.instance();

// raw html block
chunks = preview.getChunks(html.join('\n'), {});
expect(chunks).to.have.lengthOf(1);
expect(chunks[0]).to.have.lengthOf(1);
expect(chunks[0][0]).to.have.property('type', 'html_block');

// Insert an empty row
html.splice(2, 0, '\n');
chunks = preview.getChunks(html.join('\n'), {});
expect(chunks).to.have.lengthOf(2);
expect(chunks[0]).to.have.lengthOf(1);
expect(chunks[0][0]).to.have.property('type', 'html_block');
expect(chunks[1][0]).to.have.property('type', 'html_block');

done();
}, 5);
});

it('sanitizes incomplete html blocks', (done) => {
let chunks;
const wrapper = shallow(
<Preview
raw={''}
pos={0}
previewLoader={previewLoader}
template={''}
/>
);

setTimeout(() => {
const preview = wrapper.instance();

chunks = preview.getChunks('<div class="foo">', {});
expect(chunks).to.have.lengthOf(1);
expect(chunks[0]).to.have.lengthOf(1);
expect(chunks[0][0]).to.have.property('type', 'html_block');
expect(chunks[0][0]).to.have.property('content', '<div></div>');

chunks = preview.getChunks('</div>', {});
expect(chunks).to.have.lengthOf(1);
expect(chunks[0]).to.have.lengthOf(1);
expect(chunks[0][0]).to.have.property('type', 'html_block');
expect(chunks[0][0]).to.have.property('content', '');

done();
}, 5);
});

it('removes front-matter YAML header from preview', (done) => {
const wrapper = mount(
<Preview
Expand Down Expand Up @@ -393,4 +329,92 @@ describe('<Preview />', () => {
done();
}, 5);
});

it('should not display iframes (#122)', (done) => {
const content = '<a href=""><iframe src="javascript:alert(1)"></iframe></a>';
const wrapper = mount(
<Preview
raw={content}
pos={0}
previewLoader={previewLoader}
template={''}
/>
);

setTimeout(() => {
expect(wrapper.html()).not.to.contain(content);

done();
}, 5);
});

it('should not render bad input tag (#122)', (done) => {
const content = '<input onfocus=alert(1) autofocus>>';
const wrapper = mount(
<Preview
raw={content}
pos={0}
previewLoader={previewLoader}
template={''}
/>
);

setTimeout(() => {
expect(wrapper.html()).not.to.contain('input onfocus="alert(1)" autofocus=""');

done();
}, 5);
});

it('should not render bad HTML tag (#122)', (done) => {
const content = '<<img onerror=alert(1) src=x/>>';
const wrapper = mount(
<Preview
raw={content}
pos={0}
previewLoader={previewLoader}
template={''}
/>
);

setTimeout(() => {
expect(wrapper.html()).not.to.contain('img onerror="alert(1)" src="x/"');

done();
}, 5);
});

it('supports FontAwesome', (done) => {
const wrapper = mount(
<Preview
raw={':fa-globe:'}
pos={0}
previewLoader={previewLoader}
template={''}
/>
);

setTimeout(() => {
expect(wrapper.html()).to.contain('<i class="fa fa-globe"></i>');

done();
}, 5);
});

it('should display links with rel="noopener"', (done) => {
const wrapper = mount(
<Preview
raw={'[foo](/url)'}
pos={0}
previewLoader={previewLoader}
template={''}
/>
);

setTimeout(() => {
expect(wrapper.html()).to.contain('<a href="/url" rel="noreferrer noopener">foo</a>');

done();
}, 5);
});
});
4 changes: 4 additions & 0 deletions app/components/loaders/Preview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export default () => {
resolve({
hljs: require('highlight.js'),
markdownIt: require('markdown-it'),
markdownItPlugins: [
require('markdown-it-fontawesome'),
require('markdown-it-modify-token'),
],
emojione: require('emojione')
});
});
Expand Down
4 changes: 4 additions & 0 deletions app/scss/components/_preview.scss
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,8 @@
vertical-align: middle;
line-height: 1.4rem;
}

.fa {
margin-right: 0.5rem;
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
"lodash.debounce": "^4.0.3",
"lodash.isequal": "^4.1.1",
"markdown-it": "^6.0.0",
"markdown-it-fontawesome": "^0.2.0",
"markdown-it-modify-token": "^1.0.2",
"mocha": "^2.4.5",
"mocha-circleci-reporter": "0.0.1",
"node-sass": "^3.4.2",
Expand All @@ -68,7 +70,6 @@
"react-addons-test-utils": "^0.14.7",
"react-dom": "^0.14.7",
"react-loader": "^2.1.0",
"sanitize-html": "^1.11.3",
"sass-loader": "^3.1.2",
"sinon": "^1.17.3",
"sjcl": "^1.0.3",
Expand Down

0 comments on commit 8cb7aa6

Please sign in to comment.