Commit 68e34351 authored by JC Brand's avatar JC Brand

Reject unencapsulated forwarded messages

since we don't support XEP-0297 on its own
parent fe34b7ea
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
## 5.0.3 (Unreleased) ## 5.0.3 (Unreleased)
- Emit `chatBoxFocused` and `chatBoxBlurred` events for emoji picker input - Emit `chatBoxFocused` and `chatBoxBlurred` events for emoji picker input
- SECURITY FIX: Reject unencapsulated forwarded messages, since we don't support XEP-0297 on its own
## 5.0.2 (2019-09-11) ## 5.0.2 (2019-09-11)
......
This diff is collapsed.
...@@ -11,6 +11,42 @@ ...@@ -11,6 +11,42 @@
describe("A Groupchat Message", function () { describe("A Groupchat Message", function () {
it("is rejected if it's an unencapsulated forwarded message",
mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
const muc_jid = 'lounge@montague.lit';
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const impersonated_jid = `${muc_jid}/alice`;
const received_stanza = u.toStanza(`
<message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
<forwarded xmlns='urn:xmpp:forward:0'>
<delay xmlns='urn:xmpp:delay' stamp='2019-07-10T23:08:25Z'/>
<message from='${impersonated_jid}'
id='0202197'
to='${_converse.bare_jid}'
type='groupchat'
xmlns='jabber:client'>
<body>Yet I should kill thee with much cherishing.</body>
</message>
</forwarded>
</message>
`);
const view = _converse.api.chatviews.get(muc_jid);
await view.model.onMessage(received_stanza);
spyOn(_converse, 'log');
_converse.connection._dataRecv(test_utils.createRequest(received_stanza));
expect(_converse.log).toHaveBeenCalledWith(
'onMessage: Ignoring unencapsulated forwarded groupchat message',
Strophe.LogLevel.WARN
);
expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
expect(view.model.messages.length).toBe(0);
done();
}));
it("is specially marked when you are mentioned in it", it("is specially marked when you are mentioned in it",
mock.initConverse( mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {}, null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
...@@ -165,7 +201,7 @@ ...@@ -165,7 +201,7 @@
expect(_converse.log).toHaveBeenCalledWith( expect(_converse.log).toHaveBeenCalledWith(
'onMessage: Ignoring XEP-0280 "groupchat" message carbon, '+ 'onMessage: Ignoring XEP-0280 "groupchat" message carbon, '+
'according to the XEP groupchat messages SHOULD NOT be carbon copied', 'according to the XEP groupchat messages SHOULD NOT be carbon copied',
Strophe.LogLevel.ERROR Strophe.LogLevel.WARN
); );
expect(view.el.querySelectorAll('.chat-msg').length).toBe(0); expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
expect(view.model.messages.length).toBe(0); expect(view.model.messages.length).toBe(0);
......
...@@ -959,15 +959,14 @@ converse.plugins.add('converse-chatboxes', { ...@@ -959,15 +959,14 @@ converse.plugins.add('converse-chatboxes', {
stanza.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE || stanza.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE ||
stanza.getElementsByTagName(_converse.GONE).length && _converse.GONE; stanza.getElementsByTagName(_converse.GONE).length && _converse.GONE;
const is_single_emoji = text ? await u.isSingleEmoji(text) : false;
const replaced_id = this.getReplaceId(stanza) const replaced_id = this.getReplaceId(stanza)
const msgid = replaced_id || stanza.getAttribute('id') || original_stanza.getAttribute('id'); const msgid = replaced_id || stanza.getAttribute('id') || original_stanza.getAttribute('id');
const attrs = Object.assign({ const attrs = Object.assign({
'chat_state': chat_state, 'chat_state': chat_state,
'is_archived': this.isArchived(original_stanza), 'is_archived': this.isArchived(original_stanza),
'is_delayed': !!delay, 'is_delayed': !!delay,
'is_single_emoji': text ? await u.isSingleEmoji(text) : false,
'is_spoiler': !!spoiler, 'is_spoiler': !!spoiler,
'is_single_emoji': is_single_emoji,
'message': text, 'message': text,
'msgid': msgid, 'msgid': msgid,
'references': this.getReferencesFromStanza(stanza), 'references': this.getReferencesFromStanza(stanza),
...@@ -1145,6 +1144,27 @@ converse.plugins.add('converse-chatboxes', { ...@@ -1145,6 +1144,27 @@ converse.plugins.add('converse-chatboxes', {
chatbox.messages.create(attrs); chatbox.messages.create(attrs);
}, },
/**
* Reject an incoming message by replying with an error message of type "cancel".
* @private
* @method _converse.ChatBox#rejectMessage
* @param { XMLElement } stanza - The incoming message stanza
* @param { XMLElement } text - Text explaining why the message was rejected
*/
rejectMessage (stanza, text) {
_converse.api.send(
$msg({
'to': stanza.getAttribute('from'),
'type': 'error',
'id': stanza.getAttribute('id')
}).c('error', {'type': 'cancel'})
.c('not-allowed', {xmlns:"urn:ietf:params:xml:ns:xmpp-stanzas"}).up()
.c('text', {xmlns:"urn:ietf:params:xml:ns:xmpp-stanzas"}).t(text)
);
_converse.log(`Rejecting message stanza with the following reason: ${text}`, Strophe.LogLevel.WARN);
_converse.log(stanza, Strophe.LogLevel.WARN);
},
/** /**
* Handler method for all incoming single-user chat "message" stanzas. * Handler method for all incoming single-user chat "message" stanzas.
* @private * @private
...@@ -1152,46 +1172,62 @@ converse.plugins.add('converse-chatboxes', { ...@@ -1152,46 +1172,62 @@ converse.plugins.add('converse-chatboxes', {
* @param { XMLElement } stanza - The incoming message stanza * @param { XMLElement } stanza - The incoming message stanza
*/ */
async onMessage (stanza) { async onMessage (stanza) {
const original_stanza = stanza;
let to_jid = stanza.getAttribute('to'); let to_jid = stanza.getAttribute('to');
const to_resource = Strophe.getResourceFromJid(to_jid); const to_resource = Strophe.getResourceFromJid(to_jid);
if (_converse.filter_by_resource && (to_resource && to_resource !== _converse.resource)) { if (_converse.filter_by_resource && (to_resource && to_resource !== _converse.resource)) {
_converse.log( return _converse.log(
`onMessage: Ignoring incoming message intended for a different resource: ${to_jid}`, `onMessage: Ignoring incoming message intended for a different resource: ${to_jid}`,
Strophe.LogLevel.INFO Strophe.LogLevel.INFO
); );
return true;
} else if (utils.isHeadlineMessage(_converse, stanza)) { } else if (utils.isHeadlineMessage(_converse, stanza)) {
// XXX: Ideally we wouldn't have to check for headline // XXX: Prosody sends headline messages with the
// messages, but Prosody sends headline messages with the
// wrong type ('chat'), so we need to filter them out here. // wrong type ('chat'), so we need to filter them out here.
_converse.log( return _converse.log(
`onMessage: Ignoring incoming headline message from JID: ${stanza.getAttribute('from')}`, `onMessage: Ignoring incoming headline message from JID: ${stanza.getAttribute('from')}`,
Strophe.LogLevel.INFO Strophe.LogLevel.INFO
); );
return true;
} }
let is_carbon = false; const bare_forward = sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length;
const forwarded = stanza.querySelector('forwarded'); if (bare_forward) {
const original_stanza = stanza; return this.rejectMessage(
stanza,
if (forwarded !== null) { 'Forwarded messages not part of an encapsulating protocol are not supported'
const xmlns = Strophe.NS.CARBONS; );
is_carbon = sizzle(`received[xmlns="${xmlns}"]`, original_stanza).length > 0; }
if (is_carbon && original_stanza.getAttribute('from') !== _converse.bare_jid) { let from_jid = stanza.getAttribute('from') || _converse.bare_jid;
// Prevent message forging via carbons const is_carbon = u.isCarbonMessage(stanza);
// https://xmpp.org/extensions/xep-0280.html#security if (is_carbon) {
return true; if (from_jid === _converse.bare_jid) {
const selector = `[xmlns="${Strophe.NS.CARBONS}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
stanza = sizzle(selector, stanza).pop();
to_jid = stanza.getAttribute('to');
from_jid = stanza.getAttribute('from');
} else {
// Prevent message forging via carbons: https://xmpp.org/extensions/xep-0280.html#security
return this.rejectMessage(stanza, 'Rejecting carbon from invalid JID');
} }
stanza = forwarded.querySelector('message'); }
const is_mam = u.isMAMMessage(stanza);
if (is_mam) {
if (from_jid === _converse.bare_jid) {
const selector = `[xmlns="${Strophe.NS.MAM}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
stanza = sizzle(selector, stanza).pop();
to_jid = stanza.getAttribute('to'); to_jid = stanza.getAttribute('to');
from_jid = stanza.getAttribute('from');
} else {
return _converse.log(
`onMessage: Ignoring alleged MAM message from ${stanza.getAttribute('from')}`,
Strophe.LogLevel.WARN
);
}
} }
const from_jid = stanza.getAttribute('from');
const from_bare_jid = Strophe.getBareJidFromJid(from_jid); const from_bare_jid = Strophe.getBareJidFromJid(from_jid);
const is_me = from_bare_jid === _converse.bare_jid; const is_me = from_bare_jid === _converse.bare_jid;
if (is_me && to_jid === null) { if (is_me && to_jid === null) {
return _converse.log( return _converse.log(
`Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`, `Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`,
...@@ -1211,7 +1247,6 @@ converse.plugins.add('converse-chatboxes', { ...@@ -1211,7 +1247,6 @@ converse.plugins.add('converse-chatboxes', {
const chatbox = this.getChatBox(contact_jid, {'nickname': roster_nick}, has_body); const chatbox = this.getChatBox(contact_jid, {'nickname': roster_nick}, has_body);
if (chatbox) { if (chatbox) {
const is_mam = sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).length > 0;
const message = await chatbox.getDuplicateMessage(stanza); const message = await chatbox.getDuplicateMessage(stanza);
if (message) { if (message) {
chatbox.updateMessage(message, original_stanza); chatbox.updateMessage(message, original_stanza);
......
...@@ -1536,22 +1536,37 @@ converse.plugins.add('converse-muc', { ...@@ -1536,22 +1536,37 @@ converse.plugins.add('converse-muc', {
*/ */
async onMessage (stanza) { async onMessage (stanza) {
const original_stanza = stanza; const original_stanza = stanza;
const is_carbon = sizzle(`received[xmlns="${Strophe.NS.CARBONS}"]`, original_stanza).length > 0; const bare_forward = sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length;
if (bare_forward) {
return _converse.log(
'onMessage: Ignoring unencapsulated forwarded groupchat message',
Strophe.LogLevel.WARN
);
}
const is_carbon = u.isCarbonMessage(stanza);
if (is_carbon) { if (is_carbon) {
// XEP-280: groupchat messages SHOULD NOT be carbon copied, so we're discarding it. // XEP-280: groupchat messages SHOULD NOT be carbon copied, so we're discarding it.
_converse.log( return _converse.log(
'onMessage: Ignoring XEP-0280 "groupchat" message carbon, '+ 'onMessage: Ignoring XEP-0280 "groupchat" message carbon, '+
'according to the XEP groupchat messages SHOULD NOT be carbon copied', 'according to the XEP groupchat messages SHOULD NOT be carbon copied',
Strophe.LogLevel.ERROR); Strophe.LogLevel.WARN
return; );
} }
const is_mam = u.isMAMMessage(stanza);
if (is_mam) {
if (original_stanza.getAttribute('from') === this.get('jid')) {
const selector = `[xmlns="${Strophe.NS.MAM}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
stanza = sizzle(selector, stanza).pop();
} else {
return _converse.log(
`onMessage: Ignoring alleged MAM groupchat message from ${stanza.getAttribute('from')}`,
Strophe.LogLevel.WARN
);
}
}
this.createInfoMessages(stanza); this.createInfoMessages(stanza);
this.fetchFeaturesIfConfigurationChanged(stanza); this.fetchFeaturesIfConfigurationChanged(stanza);
const forwarded = sizzle(`forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).pop();
if (forwarded) {
stanza = forwarded.querySelector('message');
}
const message = await this.getDuplicateMessage(original_stanza); const message = await this.getDuplicateMessage(original_stanza);
if (message) { if (message) {
......
...@@ -50,6 +50,16 @@ u.toStanza = function (string) { ...@@ -50,6 +50,16 @@ u.toStanza = function (string) {
return node.firstElementChild; return node.firstElementChild;
} }
u.isMAMMessage = function (stanza) {
return sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, stanza).length > 0;
}
u.isCarbonMessage = function (stanza) {
const xmlns = Strophe.NS.CARBONS;
return sizzle(`message > received[xmlns="${xmlns}"]`, stanza).length > 0 ||
sizzle(`message > sent[xmlns="${xmlns}"]`, stanza).length > 0;
}
u.getLongestSubstring = function (string, candidates) { u.getLongestSubstring = function (string, candidates) {
function reducer (accumulator, current_value) { function reducer (accumulator, current_value) {
if (string.startsWith(current_value)) { if (string.startsWith(current_value)) {
......
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