diff options
-rw-r--r-- | src/corelib/serialization/qxmlstream.cpp | 36 | ||||
-rw-r--r-- | src/corelib/serialization/qxmlstream.g | 14 | ||||
-rw-r--r-- | src/corelib/serialization/qxmlstream.h | 2 | ||||
-rw-r--r-- | src/corelib/serialization/qxmlstream_p.h | 14 | ||||
-rw-r--r-- | tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp | 44 |
5 files changed, 106 insertions, 4 deletions
diff --git a/src/corelib/serialization/qxmlstream.cpp b/src/corelib/serialization/qxmlstream.cpp index 7ff87885a5..d7fb0d0d41 100644 --- a/src/corelib/serialization/qxmlstream.cpp +++ b/src/corelib/serialization/qxmlstream.cpp @@ -2041,6 +2041,42 @@ QStringRef QXmlStreamReader::dtdSystemId() const return QStringRef(); } +/*! + \since 5.15 + + Returns the maximum amount of characters a single entity is + allowed to expand into. If a single entity expands past the + given limit, the document is not considered well formed. + + \sa setEntityExpansionLimit +*/ +int QXmlStreamReader::entityExpansionLimit() const +{ + Q_D(const QXmlStreamReader); + return d->entityExpansionLimit; +} + +/*! + \since 5.15 + + Sets the maximum amount of characters a single entity is + allowed to expand into to \a limit. If a single entity expands + past the given limit, the document is not considered well formed. + + The limit is there to prevent DoS attacks when loading unknown + XML documents where recursive entity expansion could otherwise + exhaust all available memory. + + The default value for this property is 4096 characters. + + \sa entityExpansionLimit +*/ +void QXmlStreamReader::setEntityExpansionLimit(int limit) +{ + Q_D(QXmlStreamReader); + d->entityExpansionLimit = limit; +} + /*! If the tokenType() is \l StartElement, this function returns the element's namespace declarations. Otherwise an empty vector is returned. diff --git a/src/corelib/serialization/qxmlstream.g b/src/corelib/serialization/qxmlstream.g index 12ecc9bdb2..b623de9505 100644 --- a/src/corelib/serialization/qxmlstream.g +++ b/src/corelib/serialization/qxmlstream.g @@ -285,9 +285,19 @@ public: QHash<QStringView, Entity> entityHash; QHash<QStringView, Entity> parameterEntityHash; QXmlStreamSimpleStack<Entity *>entityReferenceStack; + int entityExpansionLimit = 4096; + int entityLength = 0; inline bool referenceEntity(Entity &entity) { if (entity.isCurrentlyReferenced) { - raiseWellFormedError(QXmlStream::tr("Recursive entity detected.")); + raiseWellFormedError(QXmlStream::tr("Self-referencing entity detected.")); + return false; + } + // entityLength represents the amount of additional characters the + // entity expands into (can be negative for e.g. &). It's used to + // avoid DoS attacks through recursive entity expansions + entityLength += entity.value.size() - entity.name.size() - 2; + if (entityLength > entityExpansionLimit) { + raiseWellFormedError(QXmlStream::tr("Entity expands to more characters than the entity expansion limit.")); return false; } entity.isCurrentlyReferenced = true; @@ -838,6 +848,8 @@ entity_done ::= ENTITY_DONE; /. case $rule_number: entityReferenceStack.pop()->isCurrentlyReferenced = false; + if (entityReferenceStack.isEmpty()) + entityLength = 0; clearSym(); break; ./ diff --git a/src/corelib/serialization/qxmlstream.h b/src/corelib/serialization/qxmlstream.h index 7d0aa64570..c8647e0465 100644 --- a/src/corelib/serialization/qxmlstream.h +++ b/src/corelib/serialization/qxmlstream.h @@ -426,6 +426,8 @@ public: QStringRef dtdPublicId() const; QStringRef dtdSystemId() const; + int entityExpansionLimit() const; + void setEntityExpansionLimit(int limit); enum Error { NoError, diff --git a/src/corelib/serialization/qxmlstream_p.h b/src/corelib/serialization/qxmlstream_p.h index 9c94e6d434..103b123b10 100644 --- a/src/corelib/serialization/qxmlstream_p.h +++ b/src/corelib/serialization/qxmlstream_p.h @@ -774,9 +774,19 @@ public: QHash<QStringView, Entity> entityHash; QHash<QStringView, Entity> parameterEntityHash; QXmlStreamSimpleStack<Entity *>entityReferenceStack; + int entityExpansionLimit = 4096; + int entityLength = 0; inline bool referenceEntity(Entity &entity) { if (entity.isCurrentlyReferenced) { - raiseWellFormedError(QXmlStream::tr("Recursive entity detected.")); + raiseWellFormedError(QXmlStream::tr("Self-referencing entity detected.")); + return false; + } + // entityLength represents the amount of additional characters the + // entity expands into (can be negative for e.g. &). It's used to + // avoid DoS attacks through recursive entity expansions + entityLength += entity.value.size() - entity.name.size() - 2; + if (entityLength > entityExpansionLimit) { + raiseWellFormedError(QXmlStream::tr("Entity expands to more characters than the entity expansion limit.")); return false; } entity.isCurrentlyReferenced = true; @@ -1308,6 +1318,8 @@ bool QXmlStreamReaderPrivate::parse() case 10: entityReferenceStack.pop()->isCurrentlyReferenced = false; + if (entityReferenceStack.isEmpty()) + entityLength = 0; clearSym(); break; diff --git a/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp b/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp index 12279133a2..28922574b8 100644 --- a/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp +++ b/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp @@ -397,8 +397,6 @@ public: return true; } - QXmlStreamReader reader(&inputFile); - /* See testcases.dtd which reads: 'Nonvalidating parsers * must also accept "invalid" testcases, but validating ones must reject them.' */ if(type == QLatin1String("invalid") || type == QLatin1String("valid")) @@ -583,6 +581,8 @@ private slots: void roundTrip() const; void roundTrip_data() const; + void entityExpansionLimit() const; + private: static QByteArray readFile(const QString &filename); @@ -1755,6 +1755,46 @@ void tst_QXmlStream::roundTrip_data() const "</root>\n"; } +void tst_QXmlStream::entityExpansionLimit() const +{ + QString xml = QStringLiteral("<?xml version=\"1.0\"?>" + "<!DOCTYPE foo [" + "<!ENTITY a \"0123456789\" >" + "<!ENTITY b \"&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;\" >" + "<!ENTITY c \"&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;\" >" + "<!ENTITY d \"&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;\" >" + "]>" + "<foo>&d;&d;&d;</foo>"); + { + QXmlStreamReader reader(xml); + QCOMPARE(reader.entityExpansionLimit(), 4096); + do { + reader.readNext(); + } while (!reader.atEnd()); + QCOMPARE(reader.error(), QXmlStreamReader::NotWellFormedError); + } + + // &d; expands to 10k characters, minus the 3 removed (&d;) means it should fail + // with a limit of 9996 chars and pass with 9997 + { + QXmlStreamReader reader(xml); + reader.setEntityExpansionLimit(9996); + do { + reader.readNext(); + } while (!reader.atEnd()); + + QCOMPARE(reader.error(), QXmlStreamReader::NotWellFormedError); + } + { + QXmlStreamReader reader(xml); + reader.setEntityExpansionLimit(9997); + do { + reader.readNext(); + } while (!reader.atEnd()); + QCOMPARE(reader.error(), QXmlStreamReader::NoError); + } +} + void tst_QXmlStream::roundTrip() const { QFETCH(QString, in); |