summaryrefslogblamecommitdiff
path: root/user/trojita/plaintext-upgrade-attack.patch
blob: f3c30b9aae822d5ce283be946626e15632c874a2 (plain) (tree)




























































































































































































































                                                                                                                    
From 27573cf04802fc2fdeb7f7beace4612b22ea1942 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jkt@kde.org>
Date: Sun, 30 Jan 2022 23:56:31 +0100
Subject: [PATCH] IMAP: ignore unsolicited LIST and STATUS before we're
 authenticated
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

These responses were combined with legit responses which arrive from the
real server later on, after having authenticated, and as a result, a
malicious active attacker on a network was able to inject extra fake
mailboxes to the GUI even when upgrading connection from plaintext to a
secure one via STARTTLS.

These two responses are the only ones which are handled by our pre-task
code directly in Model.cpp, which means that the state machine has to be
checked and messages which are received prior to authentication must be
rejected. Since this is really a pretty serious violation of the IMAP
state machine, let's not ignore them silently, and simply treat them as
any other unexpected response.

BUG: 432353
Change-Id: I9292fcb20215ebe4dbc7a103fc9403dfa97b258b
Reported-by: Damian Poddebniak
Co-authored-by: Espen Sandøy Hustad <espen@ehustad.com>
Reviewed-by: Caspar Schutijser <caspar@schutijser.com>
---
 src/Imap/Model/Model.cpp                      |   7 +-
 tests/Imap/test_Imap_Tasks_OpenConnection.cpp | 122 +++++++++++++++++-
 tests/Imap/test_Imap_Tasks_OpenConnection.h   |   9 ++
 3 files changed, 136 insertions(+), 2 deletions(-)

diff --git a/src/Imap/Model/Model.cpp b/src/Imap/Model/Model.cpp
index 7a393c7a..79e34658 100644
--- a/src/Imap/Model/Model.cpp
+++ b/src/Imap/Model/Model.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
+/* Copyright (C) 2006 - 2021 Jan Kundrát <jkt@flaska.net>
 
    This file is part of the Trojita Qt IMAP e-mail client,
    http://trojita.flaska.net/
@@ -519,6 +519,8 @@ void Model::handleList(Imap::Parser *ptr, const Imap::Responses::List *const res
 {
     if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
         return;
+    if (accessParser(ptr).connState < CONN_STATE_AUTHENTICATED)
+        throw UnexpectedResponseReceived("Unexpected LIST response before authentication succeeded", *resp);
     accessParser(ptr).listResponses << *resp;
 }
 
@@ -547,6 +549,9 @@ void Model::handleStatus(Imap::Parser *ptr, const Imap::Responses::Status *const
 {
     if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
         return;
+    if (accessParser(ptr).connState < CONN_STATE_AUTHENTICATED)
+        throw UnexpectedResponseReceived("Unexpected STATUS response before authentication succeeded", *resp);
+
     Q_UNUSED(ptr);
     TreeItemMailbox *mailbox = findMailboxByName(resp->mailbox);
     if (! mailbox) {
diff --git a/tests/Imap/test_Imap_Tasks_OpenConnection.cpp b/tests/Imap/test_Imap_Tasks_OpenConnection.cpp
index b72898c4..953fe6f9 100644
--- a/tests/Imap/test_Imap_Tasks_OpenConnection.cpp
+++ b/tests/Imap/test_Imap_Tasks_OpenConnection.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
+/* Copyright (C) 2006 - 2021 Jan Kundrát <jkt@flaska.net>
 
    This file is part of the Trojita Qt IMAP e-mail client,
    http://trojita.flaska.net/
@@ -611,5 +611,125 @@ void ImapModelOpenConnectionTest::provideAuthDetails()
     }
 }
 
+void ImapModelOpenConnectionTest::dataBug432353()
+{
+    QTest::addColumn<QByteArray>("garbage");
+    QTest::newRow("LIST") << QByteArray("* LIST (\\HasNoChildren) \".\" x\r\n");
+    QTest::newRow("STATUS") << QByteArray("* STATUS INBOX (MESSAGES 123)\r\n");
+}
+
+void ImapModelOpenConnectionTest::testNoListStatusUponConnect_data()
+{
+    dataBug432353();
+}
+
+void ImapModelOpenConnectionTest::testNoListStatusUponConnect()
+{
+    QFETCH(QByteArray, garbage);
+    reinit(TlsRequired::Yes);
+    cEmpty();
+    QCOMPARE(model->rowCount(QModelIndex()), 1);
+    ExpectSingleErrorHere x(this);
+    cServer("* OK foo\r\n" + garbage);
+}
+
+void ImapModelOpenConnectionTest::testNoListStatusStartTls_data()
+{
+    dataBug432353();
+}
+
+void ImapModelOpenConnectionTest::testNoListStatusStartTls()
+{
+    QFETCH(QByteArray, garbage);
+    reinit(TlsRequired::Yes);
+    cEmpty();
+    cServer("* OK [Capability imap4rev1 starttls] foo\r\n");
+    QVERIFY(completedSpy->isEmpty());
+    QVERIFY(authSpy->isEmpty());
+    cClient(t.mk("STARTTLS\r\n"));
+    cServer(t.last("OK will establish secure layer immediately\r\n"));
+    QVERIFY(authSpy->isEmpty());
+    cClient("[*** STARTTLS ***]" + t.mk("CAPABILITY\r\n"));
+    QVERIFY(completedSpy->isEmpty());
+    QVERIFY(authSpy->isEmpty());
+    cServer("* CAPABILITY IMAP4rev1\r\n" + t.last("OK capability completed\r\n"));
+    cClient(t.mk("LOGIN luzr sikrit\r\n"));
+    QCOMPARE(authSpy->size(), 1);
+    ExpectSingleErrorHere x(this);
+    cServer(garbage + t.last("OK [CAPABILITY IMAP4rev1] logged in\r\n"));
+}
+
+void ImapModelOpenConnectionTest::testNoListStatusBeforeAuthenticated_data()
+{
+    dataBug432353();
+}
+
+void ImapModelOpenConnectionTest::testNoListStatusBeforeAuthenticated()
+{
+    QFETCH(QByteArray, garbage);
+    reinit(TlsRequired::Yes);
+    cEmpty();
+    cServer("* OK [Capability imap4rev1 starttls] foo\r\n");
+    QVERIFY(completedSpy->isEmpty());
+    QVERIFY(authSpy->isEmpty());
+    cClient(t.mk("STARTTLS\r\n"));
+    cServer(t.last("OK will establish secure layer immediately\r\n"));
+    QVERIFY(authSpy->isEmpty());
+    cClient("[*** STARTTLS ***]" + t.mk("CAPABILITY\r\n"));
+    QVERIFY(completedSpy->isEmpty());
+    QVERIFY(authSpy->isEmpty());
+    cServer("* CAPABILITY IMAP4rev1\r\n" + t.last("OK capability completed\r\n"));
+    cClient(t.mk("LOGIN luzr sikrit\r\n"));
+    QCOMPARE(authSpy->size(), 1);
+    ExpectSingleErrorHere x(this);
+    cServer(garbage + t.last("OK [CAPABILITY IMAP4rev1] logged in\r\n"));
+}
+
+void ImapModelOpenConnectionTest::testListStatusUnsolicited()
+{
+    model->cache()->setChildMailboxes(QString(),
+                             QList<Imap::Mailbox::MailboxMetadata>()
+                                << Imap::Mailbox::MailboxMetadata(QLatin1String("INBOX"), QString(), QStringList())
+                             );
+
+    reinit(TlsRequired::Yes);
+    cEmpty();
+    cServer("* OK [Capability imap4rev1 starttls] foo\r\n");
+    QVERIFY(completedSpy->isEmpty());
+    QVERIFY(authSpy->isEmpty());
+    cClient(t.mk("STARTTLS\r\n"));
+    cServer(t.last("OK will establish secure layer immediately\r\n"));
+    QVERIFY(authSpy->isEmpty());
+    cClient("[*** STARTTLS ***]" + t.mk("CAPABILITY\r\n"));
+    QVERIFY(completedSpy->isEmpty());
+    QVERIFY(authSpy->isEmpty());
+    cServer("* CAPABILITY IMAP4rev1\r\n" + t.last("OK capability completed\r\n"));
+    cClient(t.mk("LOGIN luzr sikrit\r\n"));
+    QCOMPARE(authSpy->size(), 1);
+    cServer(t.last("OK [CAPABILITY IMAP4rev1] logged in\r\n") +
+            "* LIST (\\HasNoChildren) \".\" abc\r\n"
+            "* LIST (\\HasNoChildren) \".\" def\r\n"
+            "* STATUS INBOX (MESSAGES 123)\r\n"
+            );
+    QCOMPARE(model->rowCount(QModelIndex()), 1);
+    cClient(t.mk("LIST \"\" \"%\"\r\n"));
+    cEmpty();
+    QCOMPARE(completedSpy->size(), 1);
+    QVERIFY(failedSpy->isEmpty());
+    cServer("* LIST (\\Noselect \\HasChildren) \".\" \"xyz\"\r\n"
+            "* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n"
+            + t.last("OK list done\r\n")
+            + "* STATUS INBOX (MESSAGES 789)\r\n");
+    cEmpty();
+
+    // Mailboxes "abc" and "def" were reported by the server as an async replay after having authenticated,
+    // so there's no reason not to trust them. However, they were received before a LIST was issues,
+    // so they should be probably ignored -- but that is tricky to do properly with command pipelining, etc.
+    QCOMPARE(model->rowCount( QModelIndex() ), 5 /* empty root, INBOX, abc, def, xyz */);
+    QCOMPARE(model->index(1, 0, QModelIndex()).data().toString(), QLatin1String("INBOX"));
+    QCOMPARE(model->index(2, 0, QModelIndex()).data().toString(), QLatin1String("abc"));
+    QCOMPARE(model->index(3, 0, QModelIndex()).data().toString(), QLatin1String("def"));
+    QCOMPARE(model->index(4, 0, QModelIndex()).data().toString(), QLatin1String("xyz"));
+}
 
 QTEST_GUILESS_MAIN( ImapModelOpenConnectionTest )
diff --git a/tests/Imap/test_Imap_Tasks_OpenConnection.h b/tests/Imap/test_Imap_Tasks_OpenConnection.h
index 4df65c05..6a0e2704 100644
--- a/tests/Imap/test_Imap_Tasks_OpenConnection.h
+++ b/tests/Imap/test_Imap_Tasks_OpenConnection.h
@@ -70,6 +70,15 @@ private slots:
 
     void testExcessivePasswordPrompts();
 
+    void dataBug432353();
+    void testNoListStatusUponConnect();
+    void testNoListStatusUponConnect_data();
+    void testNoListStatusStartTls();
+    void testNoListStatusStartTls_data();
+    void testNoListStatusBeforeAuthenticated();
+    void testNoListStatusBeforeAuthenticated_data();
+    void testListStatusUnsolicited();
+
     void provideAuthDetails();
     void acceptSsl(const QList<QSslCertificate> &certificateChain, const QList<QSslError> &sslErrors);
 
-- 
GitLab