diff options
author | Mårten Nordheim <marten.nordheim@qt.io> | 2020-08-19 12:47:47 +0200 |
---|---|---|
committer | Mårten Nordheim <marten.nordheim@qt.io> | 2020-09-22 19:08:53 +0200 |
commit | a07f35409bc1e129b027fc7ccb312949a454f66e (patch) | |
tree | 84cde42e8d76a0eb0abbaddf474975eb8393a57f | |
parent | 16a1ddd73337c2622499c77b12de9395d43aba87 (diff) |
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 <thiago.macieira@intel.com>
-rw-r--r-- | src/network/access/qdecompresshelper.cpp | 57 | ||||
-rw-r--r-- | src/network/access/qdecompresshelper_p.h | 9 | ||||
-rw-r--r-- | src/network/access/qhttp2protocolhandler.cpp | 2 | ||||
-rw-r--r-- | src/network/access/qhttpnetworkreply.cpp | 2 | ||||
-rw-r--r-- | src/network/access/qhttpnetworkrequest.cpp | 11 | ||||
-rw-r--r-- | src/network/access/qhttpnetworkrequest_p.h | 4 | ||||
-rw-r--r-- | src/network/access/qnetworkreplyhttpimpl.cpp | 8 | ||||
-rw-r--r-- | tests/auto/network/access/qdecompresshelper/tst_qdecompresshelper.cpp | 2 | ||||
-rw-r--r-- | tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp | 4 |
9 files changed, 99 insertions, 0 deletions
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<QDecompressHelper>(); + 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<QDecompressHelper>(); + countHelper->setArchiveBombDetectionEnabled(archiveBombDetectionEnabled); countHelper->setEncoding(contentEncoding); } countHelper->feed(buffer); @@ -378,11 +383,61 @@ 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 indication that the decoder still has data left internally. @@ -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<QDecompressHelper> 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<QHttpNetworkRequestPrivate> 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<QByteArray>() + && ignoreDownloadRatio.toByteArray() == "__qdecompresshelper_ignore_download_ratio") { + httpRequest.setIgnoreDecompressionRatio(true); + } + httpRequest.setPeerVerifyName(newHttpRequest.peerVerifyName()); // Create the HTTP thread delegate diff --git a/tests/auto/network/access/qdecompresshelper/tst_qdecompresshelper.cpp b/tests/auto/network/access/qdecompresshelper/tst_qdecompresshelper.cpp index 7a3aa37a47..23040b7624 100644 --- a/tests/auto/network/access/qdecompresshelper/tst_qdecompresshelper.cpp +++ b/tests/auto/network/access/qdecompresshelper/tst_qdecompresshelper.cpp @@ -372,6 +372,7 @@ void tst_QDecompressHelper::decompressBigData() const qint64 third = file.bytesAvailable() / 3; QDecompressHelper helper; + helper.setArchiveBombDetectionEnabled(false); QFETCH(QByteArray, encoding); helper.setEncoding(encoding); @@ -401,6 +402,7 @@ void tst_QDecompressHelper::bigZlib() QByteArray compressedData = file.readAll(); QDecompressHelper helper; + helper.setArchiveBombDetectionEnabled(false); helper.setEncoding("deflate"); auto firstHalf = compressedData.left(compressedData.size() - 2); helper.feed(firstHalf); diff --git a/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp b/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp index 0766cd26fc..365de831ab 100644 --- a/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp +++ b/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp @@ -7013,6 +7013,10 @@ void tst_QNetworkReply::qtbug12908compressedHttpReply() server.doClose = true; QNetworkRequest request(QUrl("http://localhost:" + QString::number(server.serverPort()))); + // QDecompressHelper will abort the download if the compressed to decompressed size ratio + // differs too much, so we override it + request.setAttribute(QNetworkRequest::Attribute(QNetworkRequest::User - 1), + QByteArray("__qdecompresshelper_ignore_download_ratio")); QNetworkReplyPtr reply(manager.get(request)); QVERIFY2(waitForFinish(reply) == Success, msgWaitForFinished(reply)); |