summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMårten Nordheim <marten.nordheim@qt.io>2020-08-19 12:47:47 +0200
committerMårten Nordheim <marten.nordheim@qt.io>2020-09-22 19:08:53 +0200
commita07f35409bc1e129b027fc7ccb312949a454f66e (patch)
tree84cde42e8d76a0eb0abbaddf474975eb8393a57f
parent16a1ddd73337c2622499c77b12de9395d43aba87 (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.cpp57
-rw-r--r--src/network/access/qdecompresshelper_p.h9
-rw-r--r--src/network/access/qhttp2protocolhandler.cpp2
-rw-r--r--src/network/access/qhttpnetworkreply.cpp2
-rw-r--r--src/network/access/qhttpnetworkrequest.cpp11
-rw-r--r--src/network/access/qhttpnetworkrequest_p.h4
-rw-r--r--src/network/access/qnetworkreplyhttpimpl.cpp8
-rw-r--r--tests/auto/network/access/qdecompresshelper/tst_qdecompresshelper.cpp2
-rw-r--r--tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp4
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));