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)
......
...@@ -12,6 +12,50 @@ ...@@ -12,6 +12,50 @@
describe("A Chat Message", function () { describe("A Chat Message", function () {
it("is rejected if it's an unencapsulated forwarded message",
mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await test_utils.waitForRoster(_converse, 'current', 2);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const forwarded_contact_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await test_utils.openChatBoxFor(_converse, contact_jid);
expect(_converse.api.chats.get().length).toBe(2);
const received_stanza = u.toStanza(`
<message to='${_converse.jid}' from='${contact_jid}' type='chat' id='${_converse.connection.getUniqueId()}'>
<body>A most courteous exposition!</body>
<forwarded xmlns='urn:xmpp:forward:0'>
<delay xmlns='urn:xmpp:delay' stamp='2019-07-10T23:08:25Z'/>
<message from='${forwarded_contact_jid}'
id='0202197'
to='${_converse.bare_jid}'
type='chat'
xmlns='jabber:client'>
<body>Yet I should kill thee with much cherishing.</body>
<mood xmlns='http://jabber.org/protocol/mood'>
<amorous/>
</mood>
</message>
</forwarded>
</message>
`);
_converse.connection._dataRecv(test_utils.createRequest(received_stanza));
const view = _converse.api.chatviews.get(contact_jid);
const sent_stanzas = _converse.connection.sent_stanzas;
const sent_stanza = await u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('error')).pop());
expect(Strophe.serialize(sent_stanza)).toBe(
`<message id="${received_stanza.getAttribute('id')}" to="${contact_jid}" type="error" xmlns="jabber:client">`+
'<error type="cancel">'+
'<not-allowed xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>'+
'<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">'+
'Forwarded messages not part of an encapsulating protocol are not supported</text>'+
'</error>'+
'</message>');
expect(_converse.api.chats.get().length).toBe(2);
done();
}));
it("can be sent as a correction by clicking the pencil icon", it("can be sent as a correction by clicking the pencil icon",
mock.initConverse( mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {}, null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
...@@ -307,99 +351,81 @@ ...@@ -307,99 +351,81 @@
await test_utils.waitForRoster(_converse, 'current'); await test_utils.waitForRoster(_converse, 'current');
test_utils.openControlBox(); test_utils.openControlBox();
let message, msg; let message;
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length) await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length)
spyOn(_converse.chatboxes, 'getChatBox').and.callThrough(); spyOn(_converse.chatboxes, 'getChatBox').and.callThrough();
_converse.filter_by_resource = true; _converse.filter_by_resource = true;
/* <message id='aeb213' to='juliet@capulet.lit/chamber'> let msg = $msg({
* <forwarded xmlns='urn:xmpp:forward:0'>
* <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
* <message xmlns='jabber:client'
* to='juliet@capulet.lit/balcony'
* from='romeo@montague.lit/orchard'
* type='chat'>
* <body>Call me but love, and I'll be new baptized; Henceforth I never will be Romeo.</body>
* </message>
* </forwarded>
* </message>
*/
msg = $msg({'id': 'aeb213', 'to': _converse.bare_jid})
.c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T13:08:25Z'}).up()
.c('message', {
'xmlns': 'jabber:client', 'xmlns': 'jabber:client',
'id': _converse.connection.getUniqueId(),
'to': _converse.bare_jid, 'to': _converse.bare_jid,
'from': sender_jid, 'from': sender_jid,
'type': 'chat'}) 'type': 'chat'})
.c('body').t("message") .c('body').t("message").up()
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T13:08:25Z'})
.tree(); .tree();
await _converse.chatboxes.onMessage(msg); await _converse.chatboxes.onMessage(msg);
await u.waitUntil(() => _converse.api.chats.get().length); await u.waitUntil(() => _converse.api.chats.get().length);
const view = _converse.api.chatviews.get(sender_jid); const view = _converse.api.chatviews.get(sender_jid);
msg = $msg({'id': 'aeb214', 'to': _converse.bare_jid}) msg = $msg({
.c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T22:08:25Z'}).up()
.c('message', {
'xmlns': 'jabber:client', 'xmlns': 'jabber:client',
'id': _converse.connection.getUniqueId(),
'to': _converse.bare_jid, 'to': _converse.bare_jid,
'from': sender_jid, 'from': sender_jid,
'type': 'chat'}) 'type': 'chat'})
.c('body').t("Older message") .c('body').t("Older message").up()
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T22:08:25Z'})
.tree(); .tree();
await _converse.chatboxes.onMessage(msg); await _converse.chatboxes.onMessage(msg);
await new Promise((resolve, reject) => view.once('messageInserted', resolve)); await new Promise((resolve, reject) => view.once('messageInserted', resolve));
msg = $msg({'id': 'aeb215', 'to': _converse.bare_jid}) msg = $msg({
.c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'}).up()
.c('message', {
'xmlns': 'jabber:client', 'xmlns': 'jabber:client',
'id': _converse.connection.getUniqueId(),
'to': _converse.bare_jid, 'to': _converse.bare_jid,
'from': sender_jid, 'from': sender_jid,
'type': 'chat'}) 'type': 'chat'})
.c('body').t("Inbetween message").up() .c('body').t("Inbetween message").up()
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'})
.tree(); .tree();
await _converse.chatboxes.onMessage(msg); await _converse.chatboxes.onMessage(msg);
await new Promise((resolve, reject) => view.once('messageInserted', resolve)); await new Promise((resolve, reject) => view.once('messageInserted', resolve));
msg = $msg({'id': 'aeb216', 'to': _converse.bare_jid}) msg = $msg({
.c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'}).up()
.c('message', {
'xmlns': 'jabber:client', 'xmlns': 'jabber:client',
'id': _converse.connection.getUniqueId(),
'to': _converse.bare_jid, 'to': _converse.bare_jid,
'from': sender_jid, 'from': sender_jid,
'type': 'chat'}) 'type': 'chat'})
.c('body').t("another inbetween message") .c('body').t("another inbetween message").up()
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'})
.tree(); .tree();
await _converse.chatboxes.onMessage(msg); await _converse.chatboxes.onMessage(msg);
await new Promise((resolve, reject) => view.once('messageInserted', resolve)); await new Promise((resolve, reject) => view.once('messageInserted', resolve));
msg = $msg({'id': 'aeb217', 'to': _converse.bare_jid}) msg = $msg({
.c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T12:18:23Z'}).up()
.c('message', {
'xmlns': 'jabber:client', 'xmlns': 'jabber:client',
'id': _converse.connection.getUniqueId(),
'to': _converse.bare_jid, 'to': _converse.bare_jid,
'from': sender_jid, 'from': sender_jid,
'type': 'chat'}) 'type': 'chat'})
.c('body').t("An earlier message on the next day") .c('body').t("An earlier message on the next day").up()
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T12:18:23Z'})
.tree(); .tree();
await _converse.chatboxes.onMessage(msg); await _converse.chatboxes.onMessage(msg);
await new Promise((resolve, reject) => view.once('messageInserted', resolve)); await new Promise((resolve, reject) => view.once('messageInserted', resolve));
msg = $msg({'id': 'aeb218', 'to': _converse.bare_jid}) msg = $msg({
.c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T22:28:23Z'}).up()
.c('message', {
'xmlns': 'jabber:client', 'xmlns': 'jabber:client',
'id': _converse.connection.getUniqueId(),
'to': _converse.bare_jid, 'to': _converse.bare_jid,
'from': sender_jid, 'from': sender_jid,
'type': 'chat'}) 'type': 'chat'})
.c('body').t("newer message from the next day") .c('body').t("newer message from the next day").up()
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T22:28:23Z'})
.tree(); .tree();
await _converse.chatboxes.onMessage(msg); await _converse.chatboxes.onMessage(msg);
await new Promise((resolve, reject) => view.once('messageInserted', resolve)); await new Promise((resolve, reject) => view.once('messageInserted', resolve));
...@@ -408,7 +434,7 @@ ...@@ -408,7 +434,7 @@
// text messages are inserted correctly with // text messages are inserted correctly with
// temporary chat events in the chat contents. // temporary chat events in the chat contents.
msg = $msg({ msg = $msg({
'id': 'aeb219', 'id': _converse.connection.getUniqueId(),
'to': _converse.bare_jid, 'to': _converse.bare_jid,
'xmlns': 'jabber:client', 'xmlns': 'jabber:client',
'from': sender_jid, 'from': sender_jid,
...@@ -418,7 +444,7 @@ ...@@ -418,7 +444,7 @@
await _converse.chatboxes.onMessage(msg); await _converse.chatboxes.onMessage(msg);
msg = $msg({ msg = $msg({
'id': 'aeb220', 'id': _converse.connection.getUniqueId(),
'to': _converse.bare_jid, 'to': _converse.bare_jid,
'xmlns': 'jabber:client', 'xmlns': 'jabber:client',
'from': sender_jid, 'from': sender_jid,
...@@ -1103,7 +1129,7 @@ ...@@ -1103,7 +1129,7 @@
'from': sender_jid, 'from': sender_jid,
'to': _converse.connection.jid, 'to': _converse.connection.jid,
'type': 'chat', 'type': 'chat',
'id': (new Date()).getTime() 'id': _converse.connection.getUniqueId()
}).c('body').t("Another message 1 minute and 1 second since the previous one").up() }).c('body').t("Another message 1 minute and 1 second since the previous one").up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
await new Promise((resolve, reject) => view.once('messageInserted', resolve)); await new Promise((resolve, reject) => view.once('messageInserted', resolve));
...@@ -1130,17 +1156,15 @@ ...@@ -1130,17 +1156,15 @@
"Another message within 10 minutes, but from a different person"); "Another message within 10 minutes, but from a different person");
// Let's add a delayed, inbetween message // Let's add a delayed, inbetween message
_converse.chatboxes.onMessage($msg({'id': 'aeb218', 'to': _converse.bare_jid}) _converse.chatboxes.onMessage(
.c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) $msg({
.c('delay', {'xmlns': 'urn:xmpp:delay',
'stamp': dayjs(base_time).add(5, 'minutes').toISOString()
}).up()
.c('message', {
'xmlns': 'jabber:client', 'xmlns': 'jabber:client',
'id': _converse.connection.getUniqueId(),
'to': _converse.bare_jid, 'to': _converse.bare_jid,
'from': sender_jid, 'from': sender_jid,
'type': 'chat'}) 'type': 'chat'
.c('body').t("A delayed message, sent 5 minutes since we started") }).c('body').t("A delayed message, sent 5 minutes since we started").up()
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp': dayjs(base_time).add(5, 'minutes').toISOString()})
.tree()); .tree());
await new Promise((resolve, reject) => view.once('messageInserted', resolve)); await new Promise((resolve, reject) => view.once('messageInserted', resolve));
...@@ -1162,15 +1186,15 @@ ...@@ -1162,15 +1186,15 @@
"Another message 1 minute and 1 second since the previous one"); "Another message 1 minute and 1 second since the previous one");
expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(7)'))).toBe(false); expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(7)'))).toBe(false);
_converse.chatboxes.onMessage($msg({'id': 'aeb213', 'to': _converse.bare_jid}) _converse.chatboxes.onMessage(
.c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) $msg({
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':dayjs(base_time).add(4, 'minutes').toISOString()}).up()
.c('message', {
'xmlns': 'jabber:client', 'xmlns': 'jabber:client',
'id': _converse.connection.getUniqueId(),
'to': sender_jid, 'to': sender_jid,
'from': _converse.bare_jid+"/some-other-resource", 'from': _converse.bare_jid+"/some-other-resource",
'type': 'chat'}) 'type': 'chat'})
.c('body').t("A carbon message 4 minutes later") .c('body').t("A carbon message 4 minutes later").up()
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':dayjs(base_time).add(4, 'minutes').toISOString()})
.tree()); .tree());
await new Promise((resolve, reject) => view.once('messageInserted', resolve)); await new Promise((resolve, reject) => view.once('messageInserted', resolve));
...@@ -1259,36 +1283,6 @@ ...@@ -1259,36 +1283,6 @@
done(); done();
})); }));
it("forwarded does not emit a message delivery receipt if it's mine",
mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await test_utils.waitForRoster(_converse, 'current', 1);
const recipient_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msg_id = u.getUniqueId();
const sent_stanzas = [];
const view = await test_utils.openChatBoxFor(_converse, recipient_jid);
spyOn(view.model, 'sendReceiptStanza').and.callThrough();
const msg = $msg({
'from': converse.bare_jid,
'to': _converse.connection.jid,
'type': 'chat',
'id': u.getUniqueId(),
}).c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
.c('message', {
'xmlns': 'jabber:client',
'from': _converse.bare_jid+'/another-resource',
'to': recipient_jid,
'type': 'chat',
'id': msg_id
}).c('body').t('Message!').up()
.c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree();
await _converse.chatboxes.onMessage(msg);
await u.waitUntil(() => _converse.api.chats.get().length);
expect(view.model.sendReceiptStanza).not.toHaveBeenCalled();
done();
}));
describe("when sent", function () { describe("when sent", function () {
it("can have its delivery acknowledged by a receipt", it("can have its delivery acknowledged by a receipt",
......
...@@ -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