From 904be7f655ed29a38c982fe5b02097fc11e4f9f7 Mon Sep 17 00:00:00 2001
From: hua_yadong <huayadong@kylinos.cn>
Date: Fri, 24 Nov 2023 14:42:54 +0800
Subject: [PATCH] qtbase5.15.10-CVE-2023-38197

---
 src/corelib/serialization/qxmlstream.cpp      | 140 +++++++++++++++++-
 src/corelib/serialization/qxmlstream_p.h      |  11 ++
 .../qxmlstream/tokenError/dtdInBody.xml       |  21 +++
 .../qxmlstream/tokenError/multipleDtd.xml     |  21 +++
 .../qxmlstream/tokenError/wellFormed.xml      |  16 ++
 .../qxmlstream/tst_qxmlstream.cpp             |  39 +++++
 6 files changed, 240 insertions(+), 8 deletions(-)
 create mode 100644 tests/auto/corelib/serialization/qxmlstream/tokenError/dtdInBody.xml
 create mode 100644 tests/auto/corelib/serialization/qxmlstream/tokenError/multipleDtd.xml
 create mode 100644 tests/auto/corelib/serialization/qxmlstream/tokenError/wellFormed.xml

diff --git a/src/corelib/serialization/qxmlstream.cpp b/src/corelib/serialization/qxmlstream.cpp
index 11d162cb..f5d42e38 100644
--- a/src/corelib/serialization/qxmlstream.cpp
+++ b/src/corelib/serialization/qxmlstream.cpp
@@ -43,6 +43,7 @@
 
 #include "qxmlutils_p.h"
 #include <qdebug.h>
+#include <QtCore/private/qoffsetstringarray_p.h>
 #include <qfile.h>
 #include <stdio.h>
 #if QT_CONFIG(textcodec)
@@ -160,7 +161,7 @@ enum { StreamEOF = ~0U };
     addData() or by waiting for it to arrive on the device().
 
     \value UnexpectedElementError The parser encountered an element
-    that was different to those it expected.
+    or token that was different to those it expected.
 
 */
 
@@ -295,13 +296,34 @@ QXmlStreamEntityResolver *QXmlStreamReader::entityResolver() const
 
   QXmlStreamReader is a well-formed XML 1.0 parser that does \e not
   include external parsed entities. As long as no error occurs, the
-  application code can thus be assured that the data provided by the
-  stream reader satisfies the W3C's criteria for well-formed XML. For
-  example, you can be certain that all tags are indeed nested and
-  closed properly, that references to internal entities have been
-  replaced with the correct replacement text, and that attributes have
-  been normalized or added according to the internal subset of the
-  DTD.
+  application code can thus be assured, that
+  \list
+  \li the data provided by the stream reader satisfies the W3C's
+      criteria for well-formed XML,
+  \li tokens are provided in a valid order.
+  \endlist
+
+  Unless QXmlStreamReader raises an error, it guarantees the following:
+  \list
+  \li All tags are nested and closed properly.
+  \li References to internal entities have been replaced with the
+      correct replacement text.
+  \li Attributes have been normalized or added according to the
+      internal subset of the \l DTD.
+  \li Tokens of type \l StartDocument happen before all others,
+      aside from comments and processing instructions.
+  \li At most one DOCTYPE element (a token of type \l DTD) is present.
+  \li If present, the DOCTYPE appears before all other elements,
+      aside from StartDocument, comments and processing instructions.
+  \endlist
+
+  In particular, once any token of type \l StartElement, \l EndElement,
+  \l Characters, \l EntityReference or \l EndDocument is seen, no
+  tokens of type StartDocument or DTD will be seen. If one is present in
+  the input stream, out of order, an error is raised.
+
+  \note The token types \l Comment and \l ProcessingInstruction may appear
+  anywhere in the stream.
 
   If an error occurs while parsing, atEnd() and hasError() return
   true, and error() returns the error that occurred. The functions
@@ -620,6 +642,7 @@ QXmlStreamReader::TokenType QXmlStreamReader::readNext()
         d->token = -1;
         return readNext();
     }
+    d->checkToken();
     return d->type;
 }
 
@@ -739,6 +762,10 @@ static const short QXmlStreamReader_tokenTypeString_indices[] = {
     0, 8, 16, 30, 42, 55, 66, 77, 85, 89, 105, 0
 };
 
+static constexpr auto QXmlStreamReader_XmlContextString = qOffsetStringArray(
+    "Prolog",
+    "Body"
+);
 
 /*!
     \property  QXmlStreamReader::namespaceProcessing
@@ -775,6 +802,15 @@ QString QXmlStreamReader::tokenString() const
                          QXmlStreamReader_tokenTypeString_indices[d->type]);
 }
 
+/*!
+   \internal
+   \return \param loc (Prolog/Body) as a string.
+ */
+static constexpr QLatin1String contextString(QXmlStreamReaderPrivate::XmlContext ctxt)
+{
+    return QLatin1String(QXmlStreamReader_XmlContextString.at(static_cast<int>(ctxt)));
+}
+
 #endif // QT_NO_XMLSTREAMREADER
 
 QXmlStreamPrivateTagStack::QXmlStreamPrivateTagStack()
@@ -866,6 +902,8 @@ void QXmlStreamReaderPrivate::init()
 
     type = QXmlStreamReader::NoToken;
     error = QXmlStreamReader::NoError;
+    currentContext = XmlContext::Prolog;
+    foundDTD = false;
 }
 
 /*
@@ -4061,6 +4099,92 @@ void QXmlStreamWriter::writeCurrentToken(const QXmlStreamReader &reader)
     }
 }
 
+static bool isTokenAllowedInContext(QXmlStreamReader::TokenType type,
+                                               QXmlStreamReaderPrivate::XmlContext loc)
+{
+    switch (type) {
+    case QXmlStreamReader::StartDocument:
+    case QXmlStreamReader::DTD:
+        return loc == QXmlStreamReaderPrivate::XmlContext::Prolog;
+
+    case QXmlStreamReader::StartElement:
+    case QXmlStreamReader::EndElement:
+    case QXmlStreamReader::Characters:
+    case QXmlStreamReader::EntityReference:
+    case QXmlStreamReader::EndDocument:
+        return loc == QXmlStreamReaderPrivate::XmlContext::Body;
+
+    case QXmlStreamReader::Comment:
+    case QXmlStreamReader::ProcessingInstruction:
+        return true;
+
+    case QXmlStreamReader::NoToken:
+    case QXmlStreamReader::Invalid:
+        return false;
+    }
+
+    return false;
+}
+
+/*!
+   \internal
+   \brief QXmlStreamReader::isValidToken
+   \return \c true if \param type is a valid token type.
+   \return \c false if \param type is an unexpected token,
+   which indicates a non-well-formed or invalid XML stream.
+ */
+bool QXmlStreamReaderPrivate::isValidToken(QXmlStreamReader::TokenType type)
+{
+    // Don't change currentContext, if Invalid or NoToken occur in the prolog
+    if (type == QXmlStreamReader::Invalid || type == QXmlStreamReader::NoToken)
+        return false;
+
+    // If a token type gets rejected in the body, there is no recovery
+    const bool result = isTokenAllowedInContext(type, currentContext);
+    if (result || currentContext == XmlContext::Body)
+        return result;
+
+    // First non-Prolog token observed => switch context to body and check again.
+    currentContext = XmlContext::Body;
+    return isTokenAllowedInContext(type, currentContext);
+}
+
+/*!
+   \internal
+   Checks token type and raises an error, if it is invalid
+   in the current context (prolog/body).
+ */
+void QXmlStreamReaderPrivate::checkToken()
+{
+    Q_Q(QXmlStreamReader);
+
+    // The token type must be consumed, to keep track if the body has been reached.
+    const XmlContext context = currentContext;
+    const bool ok = isValidToken(type);
+
+    // Do nothing if an error has been raised already (going along with an unexpected token)
+    if (error != QXmlStreamReader::Error::NoError)
+        return;
+
+    if (!ok) {
+        raiseError(QXmlStreamReader::UnexpectedElementError,
+                   QStringLiteral("Unexpected token type %1 in %2.")
+                   .arg(q->tokenString(), contextString(context)));
+        return;
+    }
+
+    if (type != QXmlStreamReader::DTD)
+        return;
+
+    // Raise error on multiple DTD tokens
+    if (foundDTD) {
+        raiseError(QXmlStreamReader::UnexpectedElementError,
+                   QStringLiteral("Found second DTD token in %1.").arg(contextString(context)));
+    } else {
+        foundDTD = true;
+    }
+}
+
 /*!
  \fn bool QXmlStreamAttributes::hasAttribute(const QString &qualifiedName) const
  \since 4.5
diff --git a/src/corelib/serialization/qxmlstream_p.h b/src/corelib/serialization/qxmlstream_p.h
index b01484ca..be7b1fe6 100644
--- a/src/corelib/serialization/qxmlstream_p.h
+++ b/src/corelib/serialization/qxmlstream_p.h
@@ -804,6 +804,17 @@ public:
 #endif
     bool atEnd;
 
+    enum class XmlContext
+    {
+        Prolog,
+        Body,
+    };
+
+    XmlContext currentContext = XmlContext::Prolog;
+    bool foundDTD = false;
+    bool isValidToken(QXmlStreamReader::TokenType type);
+    void checkToken();
+
     /*!
       \sa setType()
      */
diff --git a/tests/auto/corelib/serialization/qxmlstream/tokenError/dtdInBody.xml b/tests/auto/corelib/serialization/qxmlstream/tokenError/dtdInBody.xml
new file mode 100644
index 00000000..68ef2962
--- /dev/null
+++ b/tests/auto/corelib/serialization/qxmlstream/tokenError/dtdInBody.xml
@@ -0,0 +1,21 @@
+<!DOCTYPE TEST [
+   <!ELEMENT TESTATTRIBUTE (CASE+)>
+   <!ELEMENT CASE (CLASS, FUNCTION)>
+   <!ELEMENT CLASS (#PCDATA)>
+
+   <!-- adding random ENTITY statement, as this is typical DTD content -->
+   <!ENTITY unite "&#x222a;">
+
+   <!ATTLIST CASE CLASS CDATA #REQUIRED>
+]>
+<TEST>
+  <CASE>
+    <CLASS>tst_QXmlStream</CLASS>
+  </CASE>
+  <!-- invalid DTD in XML body follows -->
+  <!DOCTYPE DTDTEST [
+    <!ELEMENT RESULT (CASE+)>
+    <!ATTLIST RESULT OUTPUT CDATA #REQUIRED>
+  ]>
+</TEST>
+
diff --git a/tests/auto/corelib/serialization/qxmlstream/tokenError/multipleDtd.xml b/tests/auto/corelib/serialization/qxmlstream/tokenError/multipleDtd.xml
new file mode 100644
index 00000000..1dbe75c4
--- /dev/null
+++ b/tests/auto/corelib/serialization/qxmlstream/tokenError/multipleDtd.xml
@@ -0,0 +1,21 @@
+<!DOCTYPE TEST [
+   <!ELEMENT TESTATTRIBUTE (CASE+)>
+   <!ELEMENT CASE (CLASS, FUNCTION, DATASET, COMMENTS)>
+   <!ELEMENT CLASS (#PCDATA)>
+
+   <!-- adding random ENTITY statements, as this is typical DTD content -->
+   <!ENTITY iff "&hArr;">
+
+   <!ATTLIST CASE CLASS CDATA #REQUIRED>
+]>
+<!-- invalid second DTD follows -->
+<!DOCTYPE SECOND [
+   <!ELEMENT SECONDATTRIBUTE (#PCDATA)>
+   <!ENTITY on "&#8728;">
+]>
+<TEST>
+  <CASE>
+    <CLASS>tst_QXmlStream</CLASS>
+  </CASE>
+</TEST>
+
diff --git a/tests/auto/corelib/serialization/qxmlstream/tokenError/wellFormed.xml b/tests/auto/corelib/serialization/qxmlstream/tokenError/wellFormed.xml
new file mode 100644
index 00000000..9dfbc0f9
--- /dev/null
+++ b/tests/auto/corelib/serialization/qxmlstream/tokenError/wellFormed.xml
@@ -0,0 +1,16 @@
+<!DOCTYPE TEST [
+   <!ELEMENT TESTATTRIBUTE (CASE+)>
+   <!ELEMENT CASE (CLASS, FUNCTION, DATASET, COMMENTS)>
+   <!ELEMENT CLASS (#PCDATA)>
+
+   <!-- adding random ENTITY statements, as this is typical DTD content -->
+   <!ENTITY unite "&#x222a;">
+
+   <!ATTLIST CASE CLASS CDATA #REQUIRED>
+]>
+<TEST>
+  <CASE>
+    <CLASS>tst_QXmlStream</CLASS>
+  </CASE>
+</TEST>
+
diff --git a/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp b/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp
index 533fcd96..f4ba808a 100644
--- a/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp
+++ b/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp
@@ -590,6 +590,9 @@ private slots:
 
     void entityExpansionLimit() const;
 
+    void tokenErrorHandling_data() const;
+    void tokenErrorHandling() const;
+
 private:
     static QByteArray readFile(const QString &filename);
 
@@ -1842,5 +1845,41 @@ void tst_QXmlStream::roundTrip() const
     QCOMPARE(out, in);
 }
 
+void tst_QXmlStream::tokenErrorHandling_data() const
+{
+    QTest::addColumn<QString>("fileName");
+    QTest::addColumn<QXmlStreamReader::Error>("expectedError");
+    QTest::addColumn<QString>("errorKeyWord");
+
+    constexpr auto invalid = QXmlStreamReader::Error::UnexpectedElementError;
+    constexpr auto valid = QXmlStreamReader::Error::NoError;
+    QTest::newRow("DtdInBody") << "dtdInBody.xml" << invalid << "DTD";
+    QTest::newRow("multipleDTD") << "multipleDtd.xml" << invalid << "second DTD";
+    QTest::newRow("wellFormed") << "wellFormed.xml" << valid << "";
+}
+
+void tst_QXmlStream::tokenErrorHandling() const
+{
+    QFETCH(const QString, fileName);
+    QFETCH(const QXmlStreamReader::Error, expectedError);
+    QFETCH(const QString, errorKeyWord);
+
+    const QDir dir(QFINDTESTDATA("tokenError"));
+    QFile file(dir.absoluteFilePath(fileName));
+
+    // Cross-compiling: File will be on host only
+    if (!file.exists())
+        QSKIP("Testfile not found.");
+
+    file.open(QIODevice::ReadOnly);
+    QXmlStreamReader reader(&file);
+    while (!reader.atEnd())
+        reader.readNext();
+
+    QCOMPARE(reader.error(), expectedError);
+    if (expectedError != QXmlStreamReader::Error::NoError)
+        QVERIFY(reader.errorString().contains(errorKeyWord));
+}
+
 #include "tst_qxmlstream.moc"
 // vim: et:ts=4:sw=4:sts=4
-- 
2.41.0

