mirror of https://github.com/zulip/zulip.git
CVE-2018-9986: Fix XSS issues with frontend markdown processor.
This fixes a set of XSS issues with Zulip's frontend markdown processor, which is used in a limited set of contexts, such as local echo of messages and the drafts feature. The implementation of several syntax elements, including the <em> syntax, user and stream mentions, and some others failed to properly escape the content inside the syntax. Fix this, and add tests for each corrected code path. Thanks to w2w for reporting this issue.
This commit is contained in:
parent
1207a08b36
commit
3bdc8bbaa5
|
@ -76,6 +76,12 @@ people.add({
|
||||||
email: 'leo@zulip.com',
|
email: 'leo@zulip.com',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
people.add({
|
||||||
|
full_name: 'Bobby <h1>Tables</h1>',
|
||||||
|
user_id: 103,
|
||||||
|
email: 'bobby@zulip.com',
|
||||||
|
});
|
||||||
|
|
||||||
people.initialize_current_user(cordelia.user_id);
|
people.initialize_current_user(cordelia.user_id);
|
||||||
|
|
||||||
var hamletcharacters = {
|
var hamletcharacters = {
|
||||||
|
@ -92,8 +98,16 @@ var backend = {
|
||||||
members: [],
|
members: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var edgecase_group = {
|
||||||
|
name: "Bobby <h1>Tables</h1>",
|
||||||
|
id: 3,
|
||||||
|
description: "HTML Syntax to check for Markdown edge cases.",
|
||||||
|
members: [],
|
||||||
|
};
|
||||||
|
|
||||||
global.user_groups.add(hamletcharacters);
|
global.user_groups.add(hamletcharacters);
|
||||||
global.user_groups.add(backend);
|
global.user_groups.add(backend);
|
||||||
|
global.user_groups.add(edgecase_group);
|
||||||
|
|
||||||
var stream_data = global.stream_data;
|
var stream_data = global.stream_data;
|
||||||
var denmark = {
|
var denmark = {
|
||||||
|
@ -111,8 +125,16 @@ var social = {
|
||||||
in_home_view: true,
|
in_home_view: true,
|
||||||
invite_only: true,
|
invite_only: true,
|
||||||
};
|
};
|
||||||
|
var edgecase_stream = {
|
||||||
|
subscribed: true,
|
||||||
|
color: 'green',
|
||||||
|
name: 'Bobby <h1>Tables</h1>',
|
||||||
|
stream_id: 3,
|
||||||
|
in_home_view: true,
|
||||||
|
};
|
||||||
stream_data.add_sub('Denmark', denmark);
|
stream_data.add_sub('Denmark', denmark);
|
||||||
stream_data.add_sub('social', social);
|
stream_data.add_sub('social', social);
|
||||||
|
stream_data.add_sub('Bobby <h1>Tables</h1>', edgecase_stream);
|
||||||
|
|
||||||
// Check the default behavior of fenced code blocks
|
// Check the default behavior of fenced code blocks
|
||||||
// works properly before markdown is initialized.
|
// works properly before markdown is initialized.
|
||||||
|
@ -305,6 +327,23 @@ var bugdown_data = JSON.parse(fs.readFileSync(path.join(__dirname, '../../zerver
|
||||||
{input: ':)',
|
{input: ':)',
|
||||||
expected: '<p><span class="emoji emoji-1f603" title="smiley">:smiley:</span></p>',
|
expected: '<p><span class="emoji emoji-1f603" title="smiley">:smiley:</span></p>',
|
||||||
translate_emoticons: true},
|
translate_emoticons: true},
|
||||||
|
// Test HTML Escape in Custom Zulip Rules
|
||||||
|
{input: '@**<h1>The Rogue One</h1>**',
|
||||||
|
expected: '<p>@**<h1>The Rogue One</h1>**</p>'},
|
||||||
|
{input: '#**<h1>The Rogue One</h1>**',
|
||||||
|
expected: '<p>#**<h1>The Rogue One</h1>**</p>'},
|
||||||
|
{input: '!avatar(<h1>The Rogue One</h1>)',
|
||||||
|
expected: '<p><img alt="<h1>The Rogue One</h1>" class="message_body_gravatar" src="/avatar/<h1>The Rogue One</h1>?s=30" title="<h1>The Rogue One</h1>"></p>'},
|
||||||
|
{input: ':<h1>The Rogue One</h1>:',
|
||||||
|
expected: '<p>:<h1>The Rogue One</h1>:</p>'},
|
||||||
|
{input: '@**O\'Connell**',
|
||||||
|
expected: '<p>@**O'Connell**</p>'},
|
||||||
|
{input: '@*Bobby <h1>Tables</h1>*',
|
||||||
|
expected: '<p><span class="user-group-mention" data-user-group-id="3">@Bobby <h1>Tables</h1></span></p>'},
|
||||||
|
{input: '@**Bobby <h1>Tables</h1>**',
|
||||||
|
expected: '<p><span class="user-mention" data-user-id="103">@Bobby <h1>Tables</h1></span></p>'},
|
||||||
|
{input: '#**Bobby <h1>Tables</h1>**',
|
||||||
|
expected: '<p><a class="stream" data-stream-id="3" href="http://zulip.zulipdev.com/#narrow/stream/3-Bobby-.3Ch1.3ETables.3C.2Fh1.3E">#Bobby <h1>Tables</h1></a></p>'},
|
||||||
];
|
];
|
||||||
|
|
||||||
// We remove one of the unicode emoji we put as input in one of the test
|
// We remove one of the unicode emoji we put as input in one of the test
|
||||||
|
@ -322,7 +361,6 @@ var bugdown_data = JSON.parse(fs.readFileSync(path.join(__dirname, '../../zerver
|
||||||
var message = {raw_content: input};
|
var message = {raw_content: input};
|
||||||
markdown.apply_markdown(message);
|
markdown.apply_markdown(message);
|
||||||
var output = message.content;
|
var output = message.content;
|
||||||
|
|
||||||
assert.equal(expected, output);
|
assert.equal(expected, output);
|
||||||
});
|
});
|
||||||
}());
|
}());
|
||||||
|
|
|
@ -11,6 +11,17 @@ var exports = {};
|
||||||
var realm_filter_map = {};
|
var realm_filter_map = {};
|
||||||
var realm_filter_list = [];
|
var realm_filter_list = [];
|
||||||
|
|
||||||
|
|
||||||
|
// Helper function
|
||||||
|
function escape(html, encode) {
|
||||||
|
return html
|
||||||
|
.replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
// Regexes that match some of our common bugdown markup
|
// Regexes that match some of our common bugdown markup
|
||||||
var backend_only_markdown_re = [
|
var backend_only_markdown_re = [
|
||||||
// Inline image previews, check for contiguous chars ending in image suffix
|
// Inline image previews, check for contiguous chars ending in image suffix
|
||||||
|
@ -55,7 +66,7 @@ exports.apply_markdown = function (message) {
|
||||||
message.mentioned_me_directly = true;
|
message.mentioned_me_directly = true;
|
||||||
}
|
}
|
||||||
return '<span class="user-mention" data-user-id="' + person.user_id + '">' +
|
return '<span class="user-mention" data-user-id="' + person.user_id + '">' +
|
||||||
'@' + person.full_name +
|
'@' + escape(person.full_name, true) +
|
||||||
'</span>';
|
'</span>';
|
||||||
} else if (name === 'all' || name === 'everyone' || name === 'stream') {
|
} else if (name === 'all' || name === 'everyone' || name === 'stream') {
|
||||||
message.mentioned = true;
|
message.mentioned = true;
|
||||||
|
@ -72,7 +83,7 @@ exports.apply_markdown = function (message) {
|
||||||
message.mentioned = true;
|
message.mentioned = true;
|
||||||
}
|
}
|
||||||
return '<span class="user-group-mention" data-user-group-id="' + group.id + '">' +
|
return '<span class="user-group-mention" data-user-group-id="' + group.id + '">' +
|
||||||
'@' + group.name +
|
'@' + escape(group.name, true) +
|
||||||
'</span>';
|
'</span>';
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
@ -117,15 +128,6 @@ exports.is_status_message = function (raw_content, content) {
|
||||||
content.lastIndexOf('</p>') === content.length - 4);
|
content.lastIndexOf('</p>') === content.length - 4);
|
||||||
};
|
};
|
||||||
|
|
||||||
function escape(html, encode) {
|
|
||||||
return html
|
|
||||||
.replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUnicodeEmoji(unicode_emoji) {
|
function handleUnicodeEmoji(unicode_emoji) {
|
||||||
var codepoint = unicode_emoji.codePointAt(0).toString(16);
|
var codepoint = unicode_emoji.codePointAt(0).toString(16);
|
||||||
if (emoji_codes.codepoint_to_name.hasOwnProperty(codepoint)) {
|
if (emoji_codes.codepoint_to_name.hasOwnProperty(codepoint)) {
|
||||||
|
@ -170,7 +172,7 @@ function handleStream(streamName) {
|
||||||
var href = window.location.origin + '/#narrow/stream/' + hash_util.encode_stream_name(stream.name);
|
var href = window.location.origin + '/#narrow/stream/' + hash_util.encode_stream_name(stream.name);
|
||||||
return '<a class="stream" data-stream-id="' + stream.stream_id + '" ' +
|
return '<a class="stream" data-stream-id="' + stream.stream_id + '" ' +
|
||||||
'href="' + href + '"' +
|
'href="' + href + '"' +
|
||||||
'>' + '#' + stream.name + '</a>';
|
'>' + '#' + escape(stream.name) + '</a>';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -736,7 +736,7 @@ InlineLexer.prototype.output = function(src) {
|
||||||
// em
|
// em
|
||||||
if (cap = this.rules.em.exec(src)) {
|
if (cap = this.rules.em.exec(src)) {
|
||||||
src = src.substring(cap[0].length);
|
src = src.substring(cap[0].length);
|
||||||
out += this.renderer.em(cap[1] + cap[2]);
|
out += this.renderer.em(this.output(cap[1] + cap[2]));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -825,6 +825,7 @@ InlineLexer.prototype.outputLink = function(cap, link) {
|
||||||
: this.renderer.image(href, title, escape(cap[1]));
|
: this.renderer.image(href, title, escape(cap[1]));
|
||||||
};
|
};
|
||||||
InlineLexer.prototype.emoji = function (name) {
|
InlineLexer.prototype.emoji = function (name) {
|
||||||
|
name = escape(name)
|
||||||
if (typeof this.options.emojiHandler !== 'function')
|
if (typeof this.options.emojiHandler !== 'function')
|
||||||
return ':' + name + ':';
|
return ':' + name + ':';
|
||||||
|
|
||||||
|
@ -832,6 +833,7 @@ InlineLexer.prototype.emoji = function (name) {
|
||||||
};
|
};
|
||||||
|
|
||||||
InlineLexer.prototype.unicodeEmoji = function (name) {
|
InlineLexer.prototype.unicodeEmoji = function (name) {
|
||||||
|
name = escape(name)
|
||||||
if (typeof this.options.unicodeEmojiHandler !== 'function')
|
if (typeof this.options.unicodeEmojiHandler !== 'function')
|
||||||
return name;
|
return name;
|
||||||
return this.options.unicodeEmojiHandler(name);
|
return this.options.unicodeEmojiHandler(name);
|
||||||
|
@ -844,12 +846,14 @@ InlineLexer.prototype.tex = function (tex, fullmatch) {
|
||||||
};
|
};
|
||||||
|
|
||||||
InlineLexer.prototype.userAvatar = function (email) {
|
InlineLexer.prototype.userAvatar = function (email) {
|
||||||
|
email = escape(email);
|
||||||
if (typeof this.options.avatarHandler !== 'function')
|
if (typeof this.options.avatarHandler !== 'function')
|
||||||
return '!avatar(' + email + ')';
|
return '!avatar(' + email + ')';
|
||||||
return this.options.avatarHandler(email);
|
return this.options.avatarHandler(email);
|
||||||
};
|
};
|
||||||
|
|
||||||
InlineLexer.prototype.userGravatar = function (email) {
|
InlineLexer.prototype.userGravatar = function (email) {
|
||||||
|
email = escape(email);
|
||||||
if (typeof this.options.avatarHandler !== 'function')
|
if (typeof this.options.avatarHandler !== 'function')
|
||||||
return '!gravatar(' + email + ')';
|
return '!gravatar(' + email + ')';
|
||||||
return this.options.avatarHandler(email);
|
return this.options.avatarHandler(email);
|
||||||
|
@ -863,6 +867,7 @@ InlineLexer.prototype.realm_filter = function (filter, matches, orig) {
|
||||||
};
|
};
|
||||||
|
|
||||||
InlineLexer.prototype.usermention = function (username, orig) {
|
InlineLexer.prototype.usermention = function (username, orig) {
|
||||||
|
orig = escape(orig);
|
||||||
if (typeof this.options.userMentionHandler !== 'function')
|
if (typeof this.options.userMentionHandler !== 'function')
|
||||||
{
|
{
|
||||||
return orig;
|
return orig;
|
||||||
|
@ -877,6 +882,7 @@ InlineLexer.prototype.usermention = function (username, orig) {
|
||||||
};
|
};
|
||||||
|
|
||||||
InlineLexer.prototype.groupmention = function (groupname, orig) {
|
InlineLexer.prototype.groupmention = function (groupname, orig) {
|
||||||
|
orig = escape(orig);
|
||||||
if (typeof this.options.groupMentionHandler !== 'function')
|
if (typeof this.options.groupMentionHandler !== 'function')
|
||||||
{
|
{
|
||||||
return orig;
|
return orig;
|
||||||
|
@ -891,6 +897,7 @@ InlineLexer.prototype.groupmention = function (groupname, orig) {
|
||||||
};
|
};
|
||||||
|
|
||||||
InlineLexer.prototype.stream = function (streamName, orig) {
|
InlineLexer.prototype.stream = function (streamName, orig) {
|
||||||
|
orig = escape(orig);
|
||||||
if (typeof this.options.streamHandler !== 'function')
|
if (typeof this.options.streamHandler !== 'function')
|
||||||
return orig;
|
return orig;
|
||||||
|
|
||||||
|
@ -1031,6 +1038,7 @@ Renderer.prototype.strong = function(text) {
|
||||||
};
|
};
|
||||||
|
|
||||||
Renderer.prototype.em = function(text) {
|
Renderer.prototype.em = function(text) {
|
||||||
|
text = escape(text);
|
||||||
return '<em>' + text + '</em>';
|
return '<em>' + text + '</em>';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -216,6 +216,11 @@
|
||||||
"expected_output": "<p>A <em>foo bar</em> is a <em>baz quux</em></p>",
|
"expected_output": "<p>A <em>foo bar</em> is a <em>baz quux</em></p>",
|
||||||
"text_content": "A foo bar is a baz quux"
|
"text_content": "A foo bar is a baz quux"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "emphasis_with_html",
|
||||||
|
"input": "*<h1>Hello World</h1>*",
|
||||||
|
"expected_output": "<p><em><h1>Hello World</h1></em></p>"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "underscore_strong_disabled",
|
"name": "underscore_strong_disabled",
|
||||||
"input": "__foo__",
|
"input": "__foo__",
|
||||||
|
@ -233,6 +238,11 @@
|
||||||
"expected_output": "<p><strong><em>foo</em></strong></p>",
|
"expected_output": "<p><strong><em>foo</em></strong></p>",
|
||||||
"text_content": "foo"
|
"text_content": "foo"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "strong_with_html",
|
||||||
|
"input": "**<h1>Hello World</h1>**",
|
||||||
|
"expected_output": "<p><strong><h1>Hello World</h1></strong></p>"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "numbered_list",
|
"name": "numbered_list",
|
||||||
"input": "1. A\n 2. B",
|
"input": "1. A\n 2. B",
|
||||||
|
|
Loading…
Reference in New Issue