فهرست منبع

Reject unencapsulated forwarded messages

since we don't support XEP-0297 on its own
JC Brand 5 سال پیش
والد
کامیت
68e34351ed
7فایلهای تغییر یافته به همراه300 افزوده شده و 209 حذف شده
  1. 1 0
      CHANGES.md
  2. 48 48
      spec/mam.js
  3. 122 128
      spec/messages.js
  4. 37 1
      spec/muc_messages.js
  5. 58 23
      src/headless/converse-chatboxes.js
  6. 24 9
      src/headless/converse-muc.js
  7. 10 0
      src/headless/utils/core.js

+ 1 - 0
CHANGES.md

@@ -3,6 +3,7 @@
 ## 5.0.3 (Unreleased)
 
 - 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)
 

+ 48 - 48
spec/mam.js

@@ -215,54 +215,54 @@
                         null, ['discoInitialized'], {},
                         async function (done, _converse) {
 
-                        await test_utils.waitForRoster(_converse, 'current', 1);
-                        const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-                        await test_utils.openChatBoxFor(_converse, contact_jid);
-                        await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
-                        const sent_IQs = _converse.connection.IQ_stanzas;
-                        const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop());
-                        const queryid = stanza.querySelector('query').getAttribute('queryid');
-                        let msg = $msg({'id': _converse.connection.getUniqueId(), 'from': 'impersonator@capulet.lit', 'to': _converse.bare_jid})
-                                    .c('result',  {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()})
-                                        .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
-                                            .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
-                                            .c('message', {
-                                                'xmlns':'jabber:client',
-                                                'to': _converse.bare_jid,
-                                                'id': _converse.connection.getUniqueId(),
-                                                'from': contact_jid,
-                                                'type':'chat'
-                                            }).c('body').t("Meet me at the dance");
-                        spyOn(_converse, 'log');
-                        _converse.connection._dataRecv(test_utils.createRequest(msg));
-                        expect(_converse.log).toHaveBeenCalledWith(`Ignoring alleged MAM message from ${msg.nodeTree.getAttribute('from')}`, Strophe.LogLevel.WARN);
-
-                        msg = $msg({'id': _converse.connection.getUniqueId(), 'to': _converse.bare_jid})
-                                    .c('result',  {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()})
-                                        .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
-                                            .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
-                                            .c('message', {
-                                                'xmlns':'jabber:client',
-                                                'to': _converse.bare_jid,
-                                                'id': _converse.connection.getUniqueId(),
-                                                'from': contact_jid,
-                                                'type':'chat'
-                                            }).c('body').t("Thrice the brinded cat hath mew'd.");
-                        _converse.connection._dataRecv(test_utils.createRequest(msg));
-
-                        const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')})
-                            .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
-                                .c('set',  {'xmlns': 'http://jabber.org/protocol/rsm'})
-                                    .c('first', {'index': '0'}).t('23452-4534-1').up()
-                                    .c('last').t('09af3-cc343-b409f').up()
-                                    .c('count').t('16');
-                        _converse.connection._dataRecv(test_utils.createRequest(iq_result));
-
-                        const view = _converse.chatboxviews.get(contact_jid);
-                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
-                        expect(view.model.messages.length).toBe(1);
-                        expect(view.model.messages.at(0).get('message')).toBe("Thrice the brinded cat hath mew'd.");
-                        done();
+                    await test_utils.waitForRoster(_converse, 'current', 1);
+                    const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+                    await test_utils.openChatBoxFor(_converse, contact_jid);
+                    await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
+                    const sent_IQs = _converse.connection.IQ_stanzas;
+                    const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop());
+                    const queryid = stanza.querySelector('query').getAttribute('queryid');
+                    let msg = $msg({'id': _converse.connection.getUniqueId(), 'from': 'impersonator@capulet.lit', 'to': _converse.bare_jid})
+                                .c('result',  {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()})
+                                    .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
+                                        .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
+                                        .c('message', {
+                                            'xmlns':'jabber:client',
+                                            'to': _converse.bare_jid,
+                                            'id': _converse.connection.getUniqueId(),
+                                            'from': contact_jid,
+                                            'type':'chat'
+                                        }).c('body').t("Meet me at the dance");
+                    spyOn(_converse, 'log');
+                    _converse.connection._dataRecv(test_utils.createRequest(msg));
+                    expect(_converse.log).toHaveBeenCalledWith(`Ignoring alleged MAM message from ${msg.nodeTree.getAttribute('from')}`, Strophe.LogLevel.WARN);
+
+                    msg = $msg({'id': _converse.connection.getUniqueId(), 'to': _converse.bare_jid})
+                                .c('result',  {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()})
+                                    .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
+                                        .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
+                                        .c('message', {
+                                            'xmlns':'jabber:client',
+                                            'to': _converse.bare_jid,
+                                            'id': _converse.connection.getUniqueId(),
+                                            'from': contact_jid,
+                                            'type':'chat'
+                                        }).c('body').t("Thrice the brinded cat hath mew'd.");
+                    _converse.connection._dataRecv(test_utils.createRequest(msg));
+
+                    const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')})
+                        .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
+                            .c('set',  {'xmlns': 'http://jabber.org/protocol/rsm'})
+                                .c('first', {'index': '0'}).t('23452-4534-1').up()
+                                .c('last').t('09af3-cc343-b409f').up()
+                                .c('count').t('16');
+                    _converse.connection._dataRecv(test_utils.createRequest(iq_result));
+
+                    const view = _converse.chatboxviews.get(contact_jid);
+                    await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                    expect(view.model.messages.length).toBe(1);
+                    expect(view.model.messages.at(0).get('message')).toBe("Thrice the brinded cat hath mew'd.");
+                    done();
                 }));
 
 

+ 122 - 128
spec/messages.js

@@ -12,6 +12,50 @@
 
     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",
             mock.initConverse(
                 null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
@@ -307,100 +351,82 @@
             await test_utils.waitForRoster(_converse, 'current');
             test_utils.openControlBox();
 
-            let message, msg;
+            let message;
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length)
             spyOn(_converse.chatboxes, 'getChatBox').and.callThrough();
             _converse.filter_by_resource = true;
 
-            /*  <message id='aeb213' to='juliet@capulet.lit/chamber'>
-             *    <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',
-                        'to': _converse.bare_jid,
-                        'from': sender_jid,
-                        'type': 'chat'})
-                    .c('body').t("message")
-                    .tree();
+            let msg = $msg({
+                    'xmlns': 'jabber:client',
+                    'id': _converse.connection.getUniqueId(),
+                    'to': _converse.bare_jid,
+                    'from': sender_jid,
+                    'type': 'chat'})
+                .c('body').t("message").up()
+                .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T13:08:25Z'})
+                .tree();
             await _converse.chatboxes.onMessage(msg);
             await u.waitUntil(() => _converse.api.chats.get().length);
             const view = _converse.api.chatviews.get(sender_jid);
 
-            msg = $msg({'id': 'aeb214', 'to': _converse.bare_jid})
-                .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',
-                        'to': _converse.bare_jid,
-                        'from': sender_jid,
-                        'type': 'chat'})
-                    .c('body').t("Older message")
-                    .tree();
+            msg = $msg({
+                    'xmlns': 'jabber:client',
+                    'id': _converse.connection.getUniqueId(),
+                    'to': _converse.bare_jid,
+                    'from': sender_jid,
+                    'type': 'chat'})
+                .c('body').t("Older message").up()
+                .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T22:08:25Z'})
+                .tree();
             await _converse.chatboxes.onMessage(msg);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
-            msg = $msg({'id': 'aeb215', 'to': _converse.bare_jid})
-                .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',
-                        'to': _converse.bare_jid,
-                        'from': sender_jid,
-                        'type': 'chat'})
-                    .c('body').t("Inbetween message").up()
-                    .tree();
+            msg = $msg({
+                    'xmlns': 'jabber:client',
+                    'id': _converse.connection.getUniqueId(),
+                    'to': _converse.bare_jid,
+                    'from': sender_jid,
+                    'type': 'chat'})
+                .c('body').t("Inbetween message").up()
+                .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'})
+                .tree();
             await _converse.chatboxes.onMessage(msg);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
-            msg = $msg({'id': 'aeb216', 'to': _converse.bare_jid})
-                .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',
-                        'to': _converse.bare_jid,
-                        'from': sender_jid,
-                        'type': 'chat'})
-                    .c('body').t("another inbetween message")
-                    .tree();
+            msg = $msg({
+                    'xmlns': 'jabber:client',
+                    'id': _converse.connection.getUniqueId(),
+                    'to': _converse.bare_jid,
+                    'from': sender_jid,
+                    'type': 'chat'})
+                .c('body').t("another inbetween message").up()
+                .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'})
+                .tree();
             await _converse.chatboxes.onMessage(msg);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
-            msg = $msg({'id': 'aeb217', 'to': _converse.bare_jid})
-                .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',
-                        'to': _converse.bare_jid,
-                        'from': sender_jid,
-                        'type': 'chat'})
-                    .c('body').t("An earlier message on the next day")
-                    .tree();
+            msg = $msg({
+                    'xmlns': 'jabber:client',
+                    'id': _converse.connection.getUniqueId(),
+                    'to': _converse.bare_jid,
+                    'from': sender_jid,
+                    'type': 'chat'})
+                .c('body').t("An earlier message on the next day").up()
+                .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T12:18:23Z'})
+                .tree();
             await _converse.chatboxes.onMessage(msg);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
-            msg = $msg({'id': 'aeb218', 'to': _converse.bare_jid})
-                .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',
-                        'to': _converse.bare_jid,
-                        'from': sender_jid,
-                        'type': 'chat'})
-                    .c('body').t("newer message from the next day")
-                    .tree();
+            msg = $msg({
+                    'xmlns': 'jabber:client',
+                    'id': _converse.connection.getUniqueId(),
+                    'to': _converse.bare_jid,
+                    'from': sender_jid,
+                    'type': 'chat'})
+                .c('body').t("newer message from the next day").up()
+                .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T22:28:23Z'})
+                .tree();
             await _converse.chatboxes.onMessage(msg);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
@@ -408,7 +434,7 @@
             // text messages are inserted correctly with
             // temporary chat events in the chat contents.
             msg = $msg({
-                    'id': 'aeb219',
+                    'id': _converse.connection.getUniqueId(),
                     'to': _converse.bare_jid,
                     'xmlns': 'jabber:client',
                     'from': sender_jid,
@@ -418,7 +444,7 @@
             await _converse.chatboxes.onMessage(msg);
 
             msg = $msg({
-                    'id': 'aeb220',
+                    'id': _converse.connection.getUniqueId(),
                     'to': _converse.bare_jid,
                     'xmlns': 'jabber:client',
                     'from': sender_jid,
@@ -1103,7 +1129,7 @@
                     'from': sender_jid,
                     'to': _converse.connection.jid,
                     '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('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
@@ -1130,18 +1156,16 @@
                 "Another message within 10 minutes, but from a different person");
 
             // Let's add a delayed, inbetween message
-            _converse.chatboxes.onMessage($msg({'id': 'aeb218', 'to': _converse.bare_jid})
-                .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                    .c('delay', {'xmlns': 'urn:xmpp:delay',
-                                    'stamp': dayjs(base_time).add(5, 'minutes').toISOString()
-                                }).up()
-                    .c('message', {
-                        'xmlns': 'jabber:client',
-                        'to': _converse.bare_jid,
-                        'from': sender_jid,
-                        'type': 'chat'})
-                    .c('body').t("A delayed message, sent 5 minutes since we started")
-                    .tree());
+            _converse.chatboxes.onMessage(
+                $msg({
+                    'xmlns': 'jabber:client',
+                    'id': _converse.connection.getUniqueId(),
+                    'to': _converse.bare_jid,
+                    'from': sender_jid,
+                    'type': 'chat'
+                }).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());
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
             expect(chat_content.querySelectorAll('.message').length).toBe(7);
@@ -1162,16 +1186,16 @@
                 "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);
 
-            _converse.chatboxes.onMessage($msg({'id': 'aeb213', 'to': _converse.bare_jid})
-                .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                    .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':dayjs(base_time).add(4, 'minutes').toISOString()}).up()
-                    .c('message', {
-                        'xmlns': 'jabber:client',
-                        'to': sender_jid,
-                        'from': _converse.bare_jid+"/some-other-resource",
-                        'type': 'chat'})
-                    .c('body').t("A carbon message 4 minutes later")
-                    .tree());
+            _converse.chatboxes.onMessage(
+                $msg({
+                    'xmlns': 'jabber:client',
+                    'id': _converse.connection.getUniqueId(),
+                    'to': sender_jid,
+                    'from': _converse.bare_jid+"/some-other-resource",
+                    'type': 'chat'})
+                .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());
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
             expect(chat_content.querySelectorAll('.message').length).toBe(8);
@@ -1259,36 +1283,6 @@
             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 () {
 
             it("can have its delivery acknowledged by a receipt",

+ 37 - 1
spec/muc_messages.js

@@ -11,6 +11,42 @@
 
     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",
             mock.initConverse(
                 null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
@@ -165,7 +201,7 @@
             expect(_converse.log).toHaveBeenCalledWith(
                 'onMessage: Ignoring XEP-0280 "groupchat" message carbon, '+
                 '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.model.messages.length).toBe(0);

+ 58 - 23
src/headless/converse-chatboxes.js

@@ -959,15 +959,14 @@ converse.plugins.add('converse-chatboxes', {
                             stanza.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE ||
                             stanza.getElementsByTagName(_converse.GONE).length && _converse.GONE;
 
-                const is_single_emoji = text ? await u.isSingleEmoji(text) : false;
                 const replaced_id = this.getReplaceId(stanza)
                 const msgid = replaced_id || stanza.getAttribute('id') || original_stanza.getAttribute('id');
                 const attrs = Object.assign({
                     'chat_state': chat_state,
                     'is_archived': this.isArchived(original_stanza),
                     'is_delayed': !!delay,
+                    'is_single_emoji': text ? await u.isSingleEmoji(text) : false,
                     'is_spoiler': !!spoiler,
-                    'is_single_emoji': is_single_emoji,
                     'message': text,
                     'msgid': msgid,
                     'references': this.getReferencesFromStanza(stanza),
@@ -1145,6 +1144,27 @@ converse.plugins.add('converse-chatboxes', {
                 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.
              * @private
@@ -1152,46 +1172,62 @@ converse.plugins.add('converse-chatboxes', {
              * @param { XMLElement } stanza - The incoming message stanza
              */
             async onMessage (stanza) {
+                const original_stanza = stanza;
                 let to_jid = stanza.getAttribute('to');
                 const to_resource = Strophe.getResourceFromJid(to_jid);
 
                 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}`,
                         Strophe.LogLevel.INFO
                     );
-                    return true;
                 } else if (utils.isHeadlineMessage(_converse, stanza)) {
-                    // XXX: Ideally we wouldn't have to check for headline
-                    // messages, but Prosody sends headline messages with the
+                    // XXX: Prosody sends headline messages with the
                     // 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')}`,
                         Strophe.LogLevel.INFO
                     );
-                    return true;
                 }
 
-                let is_carbon = false;
-                const forwarded = stanza.querySelector('forwarded');
-                const original_stanza = stanza;
+                const bare_forward = sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length;
+                if (bare_forward) {
+                    return this.rejectMessage(
+                        stanza,
+                        'Forwarded messages not part of an encapsulating protocol are not supported'
+                    );
+                }
+                let from_jid = stanza.getAttribute('from') || _converse.bare_jid;
+                const is_carbon = u.isCarbonMessage(stanza);
+                if (is_carbon) {
+                    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');
+                    }
+                }
 
-                if (forwarded !== null) {
-                    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) {
-                        // Prevent message forging via carbons
-                        // https://xmpp.org/extensions/xep-0280.html#security
-                        return true;
+                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');
+                        from_jid = stanza.getAttribute('from');
+                    } else {
+                        return _converse.log(
+                            `onMessage: Ignoring alleged MAM message from ${stanza.getAttribute('from')}`,
+                            Strophe.LogLevel.WARN
+                        );
                     }
-                    stanza = forwarded.querySelector('message');
-                    to_jid = stanza.getAttribute('to');
                 }
 
-                const from_jid = stanza.getAttribute('from');
                 const from_bare_jid = Strophe.getBareJidFromJid(from_jid);
                 const is_me = from_bare_jid === _converse.bare_jid;
-
                 if (is_me && to_jid === null) {
                     return _converse.log(
                         `Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`,
@@ -1211,7 +1247,6 @@ converse.plugins.add('converse-chatboxes', {
                 const chatbox = this.getChatBox(contact_jid, {'nickname': roster_nick}, has_body);
 
                 if (chatbox) {
-                    const is_mam = sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).length > 0;
                     const message = await chatbox.getDuplicateMessage(stanza);
                     if (message) {
                         chatbox.updateMessage(message, original_stanza);

+ 24 - 9
src/headless/converse-muc.js

@@ -1536,22 +1536,37 @@ converse.plugins.add('converse-muc', {
              */
             async onMessage (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) {
                     // 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, '+
                         'according to the XEP groupchat messages SHOULD NOT be carbon copied',
-                        Strophe.LogLevel.ERROR);
-                    return;
+                        Strophe.LogLevel.WARN
+                    );
                 }
+                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.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);
                 if (message) {

+ 10 - 0
src/headless/utils/core.js

@@ -50,6 +50,16 @@ u.toStanza = function (string) {
     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) {
     function reducer (accumulator, current_value) {
         if (string.startsWith(current_value)) {