From a07f35409bc1e129b027fc7ccb312949a454f66e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Nordheim?= Date: Wed, 19 Aug 2020 12:47:47 +0200 Subject: QDecompressHelper: limit decompression ratio To avoid potential decompression bombs. This is implemented with just a simple check that the ratio doesn't pass some hardcoded preset. Change-Id: I17246f0f43e73280cdb35a8f03d65885f5678ad6 Reviewed-by: Thiago Macieira --- src/network/access/qdecompresshelper.cpp | 57 ++++++++++++++++++++++++++++ src/network/access/qdecompresshelper_p.h | 9 +++++ src/network/access/qhttp2protocolhandler.cpp | 2 + src/network/access/qhttpnetworkreply.cpp | 2 + src/network/access/qhttpnetworkrequest.cpp | 11 ++++++ src/network/access/qhttpnetworkrequest_p.h | 4 ++ src/network/access/qnetworkreplyhttpimpl.cpp | 8 ++++ 7 files changed, 93 insertions(+) (limited to 'src/network') diff --git a/src/network/access/qdecompresshelper.cpp b/src/network/access/qdecompresshelper.cpp index 66ac70fec5..981153a852 100644 --- a/src/network/access/qdecompresshelper.cpp +++ b/src/network/access/qdecompresshelper.cpp @@ -260,6 +260,7 @@ void QDecompressHelper::feed(const QByteArray &data) void QDecompressHelper::feed(QByteArray &&data) { Q_ASSERT(contentEncoding != None); + totalCompressedBytes += data.size(); if (!countInternal(data)) clear(); // If our counting brother failed then so will we :| else @@ -273,6 +274,7 @@ void QDecompressHelper::feed(QByteArray &&data) void QDecompressHelper::feed(const QByteDataBuffer &buffer) { Q_ASSERT(contentEncoding != None); + totalCompressedBytes += buffer.byteAmount(); if (!countInternal(buffer)) clear(); // If our counting brother failed then so will we :| else @@ -286,6 +288,7 @@ void QDecompressHelper::feed(const QByteDataBuffer &buffer) void QDecompressHelper::feed(QByteDataBuffer &&buffer) { Q_ASSERT(contentEncoding != None); + totalCompressedBytes += buffer.byteAmount(); if (!countInternal(buffer)) clear(); // If our counting brother failed then so will we :| else @@ -325,6 +328,7 @@ bool QDecompressHelper::countInternal(const QByteArray &data) if (countDecompressed) { if (!countHelper) { countHelper = std::make_unique(); + countHelper->setArchiveBombDetectionEnabled(archiveBombDetectionEnabled); countHelper->setEncoding(contentEncoding); } countHelper->feed(data); @@ -342,6 +346,7 @@ bool QDecompressHelper::countInternal(const QByteDataBuffer &buffer) if (countDecompressed) { if (!countHelper) { countHelper = std::make_unique(); + countHelper->setArchiveBombDetectionEnabled(archiveBombDetectionEnabled); countHelper->setEncoding(contentEncoding); } countHelper->feed(buffer); @@ -378,9 +383,59 @@ qsizetype QDecompressHelper::read(char *data, qsizetype maxSize) clear(); else if (countDecompressed) uncompressedBytes -= bytesRead; + + totalUncompressedBytes += bytesRead; + if (isPotentialArchiveBomb()) + return -1; + return bytesRead; } +/*! + \internal + Disables or enables checking the decompression ratio of archives + according to the value of \a enable. + Only for enabling us to test handling of large decompressed files + without needing to bundle large compressed files. +*/ +void QDecompressHelper::setArchiveBombDetectionEnabled(bool enable) +{ + archiveBombDetectionEnabled = enable; + if (countHelper) + countHelper->setArchiveBombDetectionEnabled(enable); +} + +bool QDecompressHelper::isPotentialArchiveBomb() const +{ + if (!archiveBombDetectionEnabled) + return false; + + if (totalCompressedBytes == 0) + return false; + + // Some protection against malicious or corrupted compressed files that expand far more than + // is reasonable. + double ratio = double(totalUncompressedBytes) / double(totalCompressedBytes); + switch (contentEncoding) { + case None: + Q_UNREACHABLE(); + break; + case Deflate: + case GZip: + if (ratio > 40) { + return true; + } + break; + case Brotli: + case Zstandard: + if (ratio > 100) { + return true; + } + break; + } + return false; +} + /*! \internal Returns true if there are encoded bytes left or there is some @@ -443,6 +498,8 @@ void QDecompressHelper::clear() countDecompressed = false; countHelper.reset(); uncompressedBytes = 0; + totalUncompressedBytes = 0; + totalCompressedBytes = 0; } qsizetype QDecompressHelper::readZLib(char *data, const qsizetype maxSize) diff --git a/src/network/access/qdecompresshelper_p.h b/src/network/access/qdecompresshelper_p.h index 6514e7d418..4e66581022 100644 --- a/src/network/access/qdecompresshelper_p.h +++ b/src/network/access/qdecompresshelper_p.h @@ -91,10 +91,14 @@ public: void clear(); + void setArchiveBombDetectionEnabled(bool enable); + static bool isSupportedEncoding(const QByteArray &encoding); static QByteArrayList acceptedEncoding(); private: + bool isPotentialArchiveBomb() const; + bool countInternal(); bool countInternal(const QByteArray &data); bool countInternal(const QByteDataBuffer &buffer); @@ -113,6 +117,11 @@ private: std::unique_ptr countHelper; qint64 uncompressedBytes = 0; + // Used for calculating the ratio + bool archiveBombDetectionEnabled = true; + qint64 totalUncompressedBytes = 0; + qint64 totalCompressedBytes = 0; + ContentEncoding contentEncoding = None; void *decoderPointer = nullptr; diff --git a/src/network/access/qhttp2protocolhandler.cpp b/src/network/access/qhttp2protocolhandler.cpp index 44c397c882..de6b5ac18f 100644 --- a/src/network/access/qhttp2protocolhandler.cpp +++ b/src/network/access/qhttp2protocolhandler.cpp @@ -1167,6 +1167,8 @@ void QHttp2ProtocolHandler::updateStream(Stream &stream, const HPack::HttpHeader httpReplyPrivate->removeAutoDecompressHeader(); httpReplyPrivate->decompressHelper.setEncoding( httpReplyPrivate->headerField("content-encoding")); + if (httpReplyPrivate->request.ignoreDecompressionRatio()) + httpReplyPrivate->decompressHelper.setArchiveBombDetectionEnabled(false); } if (QHttpNetworkReply::isHttpRedirect(statusCode) diff --git a/src/network/access/qhttpnetworkreply.cpp b/src/network/access/qhttpnetworkreply.cpp index e11ea401d2..29aef59369 100644 --- a/src/network/access/qhttpnetworkreply.cpp +++ b/src/network/access/qhttpnetworkreply.cpp @@ -582,6 +582,8 @@ qint64 QHttpNetworkReplyPrivate::readHeader(QAbstractSocket *socket) if (autoDecompress && isCompressed()) { if (!decompressHelper.setEncoding(headerField("content-encoding"))) return -1; // Either the encoding was unsupported or the decoder could not be set up + if (request.ignoreDecompressionRatio()) + decompressHelper.setArchiveBombDetectionEnabled(false); } } return bytes; diff --git a/src/network/access/qhttpnetworkrequest.cpp b/src/network/access/qhttpnetworkrequest.cpp index 7ef033047e..c0b2167d15 100644 --- a/src/network/access/qhttpnetworkrequest.cpp +++ b/src/network/access/qhttpnetworkrequest.cpp @@ -64,6 +64,7 @@ QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate(const QHttpNetworkRequest withCredentials(other.withCredentials), ssl(other.ssl), preConnect(other.preConnect), + ignoreDecompressionRatio(other.ignoreDecompressionRatio), redirectCount(other.redirectCount), redirectPolicy(other.redirectPolicy), peerVerifyName(other.peerVerifyName) @@ -402,5 +403,15 @@ void QHttpNetworkRequest::setPeerVerifyName(const QString &peerName) d->peerVerifyName = peerName; } +bool QHttpNetworkRequest::ignoreDecompressionRatio() +{ + return d->ignoreDecompressionRatio; +} + +void QHttpNetworkRequest::setIgnoreDecompressionRatio(bool enabled) +{ + d->ignoreDecompressionRatio = enabled; +} + QT_END_NAMESPACE diff --git a/src/network/access/qhttpnetworkrequest_p.h b/src/network/access/qhttpnetworkrequest_p.h index ade71f4661..1a38b24a8a 100644 --- a/src/network/access/qhttpnetworkrequest_p.h +++ b/src/network/access/qhttpnetworkrequest_p.h @@ -149,6 +149,9 @@ public: QString peerVerifyName() const; void setPeerVerifyName(const QString &peerName); + + bool ignoreDecompressionRatio(); + void setIgnoreDecompressionRatio(bool enabled); private: QSharedDataPointer d; friend class QHttpNetworkRequestPrivate; @@ -181,6 +184,7 @@ public: bool withCredentials; bool ssl; bool preConnect; + bool ignoreDecompressionRatio = false; int redirectCount; QNetworkRequest::RedirectPolicy redirectPolicy; QString peerVerifyName; diff --git a/src/network/access/qnetworkreplyhttpimpl.cpp b/src/network/access/qnetworkreplyhttpimpl.cpp index 5ba11333bf..921b482d10 100644 --- a/src/network/access/qnetworkreplyhttpimpl.cpp +++ b/src/network/access/qnetworkreplyhttpimpl.cpp @@ -774,6 +774,14 @@ void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpReq if (request.attribute(QNetworkRequest::EmitAllUploadProgressSignalsAttribute).toBool()) emitAllUploadProgressSignals = true; + // For internal use/testing + auto ignoreDownloadRatio = + request.attribute(QNetworkRequest::Attribute(QNetworkRequest::User - 1)); + if (!ignoreDownloadRatio.isNull() && ignoreDownloadRatio.canConvert() + && ignoreDownloadRatio.toByteArray() == "__qdecompresshelper_ignore_download_ratio") { + httpRequest.setIgnoreDecompressionRatio(true); + } + httpRequest.setPeerVerifyName(newHttpRequest.peerVerifyName()); // Create the HTTP thread delegate -- cgit v1.2.3