Add an expansion limit for entities
Recursively defined entities can easily exhaust all available memory. Limit entity expansion to a default of 4096 characters to avoid DoS attacks when a user loads untrusted content. Added a setter and getter to allow modifying the expansion limit. [ChangeLog][QtCore][QXmlStream] QXmlStreamReader does now by default limit the expansion of entities to 4096 characters. Documents where a single entity expands to more characters than the limit are not considered well formed. The limit is there to avoid DoS attacks through recursively expanding entities when loading untrusted content. The limit can be changed through the QXmlStreamReader::setEntityExpansionLimit() method. Fixes: QTBUG-47417 Change-Id: I94387815d74fcf34783e136387ee57fac5ded0c9 Reviewed-by: Oswald Buddenhagen <oswald.buddenhagen@gmx.de> Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
This commit is contained in:
parent
86f7d44089
commit
fd4be84d23
@ -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.
|
||||
|
@ -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;
|
||||
./
|
||||
|
@ -426,6 +426,8 @@ public:
|
||||
QStringRef dtdPublicId() const;
|
||||
QStringRef dtdSystemId() const;
|
||||
|
||||
int entityExpansionLimit() const;
|
||||
void setEntityExpansionLimit(int limit);
|
||||
|
||||
enum Error {
|
||||
NoError,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user