QNetworkRequest: Add API to set a minimum archive bomb size

Fixes: QTBUG-91870
Change-Id: Ia23e8b8bcfdf65a91fe57e739242a355c681c9e6
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
This commit is contained in:
Mårten Nordheim 2021-05-20 14:12:39 +02:00
parent 347310eb21
commit 69982182a3
11 changed files with 80 additions and 43 deletions

View File

@ -42,6 +42,7 @@
#include <QtCore/private/qbytearray_p.h> #include <QtCore/private/qbytearray_p.h>
#include <QtCore/qiodevice.h> #include <QtCore/qiodevice.h>
#include <limits>
#include <zlib.h> #include <zlib.h>
#if QT_CONFIG(brotli) #if QT_CONFIG(brotli)
@ -328,7 +329,7 @@ bool QDecompressHelper::countInternal(const QByteArray &data)
if (countDecompressed) { if (countDecompressed) {
if (!countHelper) { if (!countHelper) {
countHelper = std::make_unique<QDecompressHelper>(); countHelper = std::make_unique<QDecompressHelper>();
countHelper->setArchiveBombDetectionEnabled(archiveBombDetectionEnabled); countHelper->setMinimumArchiveBombSize(minimumArchiveBombSize);
countHelper->setEncoding(contentEncoding); countHelper->setEncoding(contentEncoding);
} }
countHelper->feed(data); countHelper->feed(data);
@ -346,7 +347,7 @@ bool QDecompressHelper::countInternal(const QByteDataBuffer &buffer)
if (countDecompressed) { if (countDecompressed) {
if (!countHelper) { if (!countHelper) {
countHelper = std::make_unique<QDecompressHelper>(); countHelper = std::make_unique<QDecompressHelper>();
countHelper->setArchiveBombDetectionEnabled(archiveBombDetectionEnabled); countHelper->setMinimumArchiveBombSize(minimumArchiveBombSize);
countHelper->setEncoding(contentEncoding); countHelper->setEncoding(contentEncoding);
} }
countHelper->feed(buffer); countHelper->feed(buffer);
@ -393,28 +394,19 @@ qsizetype QDecompressHelper::read(char *data, qsizetype maxSize)
/*! /*!
\internal \internal
Disables or enables checking the decompression ratio of archives Set the \a threshold required before the archive bomb detection kicks in.
according to the value of \a enable. By default this is 10MB. Setting it to -1 is treated as disabling the
Only for enabling us to test handling of large decompressed files feature.
without needing to bundle large compressed files.
*/ */
void QDecompressHelper::setArchiveBombDetectionEnabled(bool enable)
{
archiveBombDetectionEnabled = enable;
if (countHelper)
countHelper->setArchiveBombDetectionEnabled(enable);
}
void QDecompressHelper::setMinimumArchiveBombSize(qint64 threshold) void QDecompressHelper::setMinimumArchiveBombSize(qint64 threshold)
{ {
if (threshold == -1)
threshold = std::numeric_limits<qint64>::max();
minimumArchiveBombSize = threshold; minimumArchiveBombSize = threshold;
} }
bool QDecompressHelper::isPotentialArchiveBomb() const bool QDecompressHelper::isPotentialArchiveBomb() const
{ {
if (!archiveBombDetectionEnabled)
return false;
if (totalCompressedBytes == 0) if (totalCompressedBytes == 0)
return false; return false;
@ -430,12 +422,16 @@ bool QDecompressHelper::isPotentialArchiveBomb() const
break; break;
case Deflate: case Deflate:
case GZip: case GZip:
// This value is mentioned in docs for
// QNetworkRequest::setMinimumArchiveBombSize, keep synchronized
if (ratio > 40) { if (ratio > 40) {
return true; return true;
} }
break; break;
case Brotli: case Brotli:
case Zstandard: case Zstandard:
// This value is mentioned in docs for
// QNetworkRequest::setMinimumArchiveBombSize, keep synchronized
if (ratio > 100) { if (ratio > 100) {
return true; return true;
} }

View File

@ -91,7 +91,6 @@ public:
void clear(); void clear();
void setArchiveBombDetectionEnabled(bool enable);
void setMinimumArchiveBombSize(qint64 threshold); void setMinimumArchiveBombSize(qint64 threshold);
static bool isSupportedEncoding(const QByteArray &encoding); static bool isSupportedEncoding(const QByteArray &encoding);
@ -119,7 +118,6 @@ private:
qint64 uncompressedBytes = 0; qint64 uncompressedBytes = 0;
// Used for calculating the ratio // Used for calculating the ratio
bool archiveBombDetectionEnabled = true;
qint64 minimumArchiveBombSize = 10 * 1024 * 1024; qint64 minimumArchiveBombSize = 10 * 1024 * 1024;
qint64 totalUncompressedBytes = 0; qint64 totalUncompressedBytes = 0;
qint64 totalCompressedBytes = 0; qint64 totalCompressedBytes = 0;

View File

@ -1237,8 +1237,8 @@ void QHttp2ProtocolHandler::updateStream(Stream &stream, const HPack::HttpHeader
httpReplyPrivate->removeAutoDecompressHeader(); httpReplyPrivate->removeAutoDecompressHeader();
httpReplyPrivate->decompressHelper.setEncoding( httpReplyPrivate->decompressHelper.setEncoding(
httpReplyPrivate->headerField("content-encoding")); httpReplyPrivate->headerField("content-encoding"));
if (httpReplyPrivate->request.ignoreDecompressionRatio()) httpReplyPrivate->decompressHelper.setMinimumArchiveBombSize(
httpReplyPrivate->decompressHelper.setArchiveBombDetectionEnabled(false); httpReplyPrivate->request.minimumArchiveBombSize());
} }
if (QHttpNetworkReply::isHttpRedirect(statusCode)) { if (QHttpNetworkReply::isHttpRedirect(statusCode)) {

View File

@ -557,8 +557,7 @@ qint64 QHttpNetworkReplyPrivate::readHeader(QAbstractSocket *socket)
if (autoDecompress && isCompressed()) { if (autoDecompress && isCompressed()) {
if (!decompressHelper.setEncoding(headerField("content-encoding"))) if (!decompressHelper.setEncoding(headerField("content-encoding")))
return -1; // Either the encoding was unsupported or the decoder could not be set up return -1; // Either the encoding was unsupported or the decoder could not be set up
if (request.ignoreDecompressionRatio()) decompressHelper.setMinimumArchiveBombSize(request.minimumArchiveBombSize());
decompressHelper.setArchiveBombDetectionEnabled(false);
} }
} }
return bytes; return bytes;

View File

@ -57,6 +57,7 @@ QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate(const QHttpNetworkRequest
customVerb(other.customVerb), customVerb(other.customVerb),
priority(other.priority), priority(other.priority),
uploadByteDevice(other.uploadByteDevice), uploadByteDevice(other.uploadByteDevice),
minimumArchiveBombSize(other.minimumArchiveBombSize),
autoDecompress(other.autoDecompress), autoDecompress(other.autoDecompress),
pipeliningAllowed(other.pipeliningAllowed), pipeliningAllowed(other.pipeliningAllowed),
http2Allowed(other.http2Allowed), http2Allowed(other.http2Allowed),
@ -64,7 +65,6 @@ QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate(const QHttpNetworkRequest
withCredentials(other.withCredentials), withCredentials(other.withCredentials),
ssl(other.ssl), ssl(other.ssl),
preConnect(other.preConnect), preConnect(other.preConnect),
ignoreDecompressionRatio(other.ignoreDecompressionRatio),
needResendWithCredentials(other.needResendWithCredentials), needResendWithCredentials(other.needResendWithCredentials),
redirectCount(other.redirectCount), redirectCount(other.redirectCount),
redirectPolicy(other.redirectPolicy), redirectPolicy(other.redirectPolicy),
@ -93,7 +93,8 @@ bool QHttpNetworkRequestPrivate::operator==(const QHttpNetworkRequestPrivate &ot
&& (preConnect == other.preConnect) && (preConnect == other.preConnect)
&& (redirectPolicy == other.redirectPolicy) && (redirectPolicy == other.redirectPolicy)
&& (peerVerifyName == other.peerVerifyName) && (peerVerifyName == other.peerVerifyName)
&& (needResendWithCredentials == other.needResendWithCredentials); && (needResendWithCredentials == other.needResendWithCredentials)
&& (minimumArchiveBombSize == other.minimumArchiveBombSize);
} }
QByteArray QHttpNetworkRequest::methodName() const QByteArray QHttpNetworkRequest::methodName() const
@ -405,14 +406,14 @@ void QHttpNetworkRequest::setPeerVerifyName(const QString &peerName)
d->peerVerifyName = peerName; d->peerVerifyName = peerName;
} }
bool QHttpNetworkRequest::ignoreDecompressionRatio() qint64 QHttpNetworkRequest::minimumArchiveBombSize() const
{ {
return d->ignoreDecompressionRatio; return d->minimumArchiveBombSize;
} }
void QHttpNetworkRequest::setIgnoreDecompressionRatio(bool enabled) void QHttpNetworkRequest::setMinimumArchiveBombSize(qint64 threshold)
{ {
d->ignoreDecompressionRatio = enabled; d->minimumArchiveBombSize = threshold;
} }
QT_END_NAMESPACE QT_END_NAMESPACE

View File

@ -150,8 +150,9 @@ public:
QString peerVerifyName() const; QString peerVerifyName() const;
void setPeerVerifyName(const QString &peerName); void setPeerVerifyName(const QString &peerName);
bool ignoreDecompressionRatio(); qint64 minimumArchiveBombSize() const;
void setIgnoreDecompressionRatio(bool enabled); void setMinimumArchiveBombSize(qint64 threshold);
private: private:
QSharedDataPointer<QHttpNetworkRequestPrivate> d; QSharedDataPointer<QHttpNetworkRequestPrivate> d;
friend class QHttpNetworkRequestPrivate; friend class QHttpNetworkRequestPrivate;
@ -177,6 +178,7 @@ public:
QByteArray customVerb; QByteArray customVerb;
QHttpNetworkRequest::Priority priority; QHttpNetworkRequest::Priority priority;
mutable QNonContiguousByteDevice* uploadByteDevice; mutable QNonContiguousByteDevice* uploadByteDevice;
qint64 minimumArchiveBombSize = 0;
bool autoDecompress; bool autoDecompress;
bool pipeliningAllowed; bool pipeliningAllowed;
bool http2Allowed; bool http2Allowed;
@ -184,7 +186,6 @@ public:
bool withCredentials; bool withCredentials;
bool ssl; bool ssl;
bool preConnect; bool preConnect;
bool ignoreDecompressionRatio = false;
bool needResendWithCredentials = false; bool needResendWithCredentials = false;
int redirectCount; int redirectCount;
QNetworkRequest::RedirectPolicy redirectPolicy; QNetworkRequest::RedirectPolicy redirectPolicy;

View File

@ -774,14 +774,7 @@ void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpReq
if (request.attribute(QNetworkRequest::EmitAllUploadProgressSignalsAttribute).toBool()) if (request.attribute(QNetworkRequest::EmitAllUploadProgressSignalsAttribute).toBool())
emitAllUploadProgressSignals = true; emitAllUploadProgressSignals = true;
// For internal use/testing httpRequest.setMinimumArchiveBombSize(newHttpRequest.minimumArchiveBombSize());
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()); httpRequest.setPeerVerifyName(newHttpRequest.peerVerifyName());
// Create the HTTP thread delegate // Create the HTTP thread delegate

View File

@ -441,6 +441,7 @@ public:
peerVerifyName = other.peerVerifyName; peerVerifyName = other.peerVerifyName;
#if QT_CONFIG(http) #if QT_CONFIG(http)
h2Configuration = other.h2Configuration; h2Configuration = other.h2Configuration;
minimumArchiveBombSize = other.minimumArchiveBombSize;
#endif #endif
transferTimeout = other.transferTimeout; transferTimeout = other.transferTimeout;
} }
@ -455,6 +456,7 @@ public:
peerVerifyName == other.peerVerifyName peerVerifyName == other.peerVerifyName
#if QT_CONFIG(http) #if QT_CONFIG(http)
&& h2Configuration == other.h2Configuration && h2Configuration == other.h2Configuration
&& minimumArchiveBombSize == other.minimumArchiveBombSize
#endif #endif
&& transferTimeout == other.transferTimeout && transferTimeout == other.transferTimeout
; ;
@ -470,6 +472,7 @@ public:
QString peerVerifyName; QString peerVerifyName;
#if QT_CONFIG(http) #if QT_CONFIG(http)
QHttp2Configuration h2Configuration; QHttp2Configuration h2Configuration;
qint64 minimumArchiveBombSize = 10ll * 1024ll * 1024ll;
#endif #endif
int transferTimeout; int transferTimeout;
}; };
@ -896,7 +899,50 @@ void QNetworkRequest::setHttp2Configuration(const QHttp2Configuration &configura
{ {
d->h2Configuration = configuration; d->h2Configuration = configuration;
} }
/*!
\since 6.2
Returns the threshold for archive bomb checks.
If the decompressed size of a reply is smaller than this, Qt will simply
decompress it, without further checking.
\sa setMinimumArchiveBombSize()
*/
qint64 QNetworkRequest::minimumArchiveBombSize() const
{
return d->minimumArchiveBombSize;
}
/*!
\since 6.2
Sets the \a threshold for archive bomb checks.
Some supported compression algorithms can, in a tiny compressed file, encode
a spectacularly huge decompressed file. This is only possible if the
decompressed content is extremely monotonous, which is seldom the case for
real files being transmitted in good faith: files exercising such insanely
high compression ratios are typically payloads of buffer-overrun attacks, or
denial-of-service (by using up too much memory) attacks. Consequently, files
that decompress to huge sizes, particularly from tiny compressed forms, are
best rejected as suspected malware.
If a reply's decompressed size is bigger than this threshold (by default,
10 MiB, i.e. 10 * 1024 * 1024), Qt will check the compression ratio: if that
is unreasonably large (40:1 for GZip and Deflate, or 100:1 for Brotli and
ZStandard), the reply will be treated as an error. Setting the threshold
to \c{-1} disables this check.
\sa minimumArchiveBombSize()
*/
void QNetworkRequest::setMinimumArchiveBombSize(qint64 threshold)
{
d->minimumArchiveBombSize = threshold;
}
#endif // QT_CONFIG(http) || defined(Q_CLANG_QDOC) #endif // QT_CONFIG(http) || defined(Q_CLANG_QDOC)
#if QT_CONFIG(http) || defined(Q_CLANG_QDOC) || defined (Q_OS_WASM) #if QT_CONFIG(http) || defined(Q_CLANG_QDOC) || defined (Q_OS_WASM)
/*! /*!
\since 5.15 \since 5.15

View File

@ -179,7 +179,11 @@ public:
#if QT_CONFIG(http) || defined(Q_CLANG_QDOC) #if QT_CONFIG(http) || defined(Q_CLANG_QDOC)
QHttp2Configuration http2Configuration() const; QHttp2Configuration http2Configuration() const;
void setHttp2Configuration(const QHttp2Configuration &configuration); void setHttp2Configuration(const QHttp2Configuration &configuration);
qint64 minimumArchiveBombSize() const;
void setMinimumArchiveBombSize(qint64 threshold);
#endif // QT_CONFIG(http) || defined(Q_CLANG_QDOC) #endif // QT_CONFIG(http) || defined(Q_CLANG_QDOC)
#if QT_CONFIG(http) || defined(Q_CLANG_QDOC) || defined (Q_OS_WASM) #if QT_CONFIG(http) || defined(Q_CLANG_QDOC) || defined (Q_OS_WASM)
int transferTimeout() const; int transferTimeout() const;
void setTransferTimeout(int timeout = DefaultTransferTimeoutConstant); void setTransferTimeout(int timeout = DefaultTransferTimeoutConstant);

View File

@ -373,7 +373,7 @@ void tst_QDecompressHelper::decompressBigData()
const qint64 third = file.bytesAvailable() / 3; const qint64 third = file.bytesAvailable() / 3;
QDecompressHelper helper; QDecompressHelper helper;
helper.setArchiveBombDetectionEnabled(false); helper.setMinimumArchiveBombSize(-1);
QFETCH(QByteArray, encoding); QFETCH(QByteArray, encoding);
helper.setEncoding(encoding); helper.setEncoding(encoding);
@ -442,7 +442,7 @@ void tst_QDecompressHelper::bigZlib()
QByteArray compressedData = file.readAll(); QByteArray compressedData = file.readAll();
QDecompressHelper helper; QDecompressHelper helper;
helper.setArchiveBombDetectionEnabled(false); helper.setMinimumArchiveBombSize(-1);
helper.setEncoding("deflate"); helper.setEncoding("deflate");
auto firstHalf = compressedData.left(compressedData.size() - 2); auto firstHalf = compressedData.left(compressedData.size() - 2);
helper.feed(firstHalf); helper.feed(firstHalf);

View File

@ -7044,8 +7044,7 @@ void tst_QNetworkReply::qtbug12908compressedHttpReply()
QNetworkRequest request(QUrl("http://localhost:" + QString::number(server.serverPort()))); QNetworkRequest request(QUrl("http://localhost:" + QString::number(server.serverPort())));
// QDecompressHelper will abort the download if the compressed to decompressed size ratio // QDecompressHelper will abort the download if the compressed to decompressed size ratio
// differs too much, so we override it // differs too much, so we override it
request.setAttribute(QNetworkRequest::Attribute(QNetworkRequest::User - 1), request.setMinimumArchiveBombSize(-1);
QByteArray("__qdecompresshelper_ignore_download_ratio"));
QNetworkReplyPtr reply(manager.get(request)); QNetworkReplyPtr reply(manager.get(request));
QVERIFY2(waitForFinish(reply) == Success, msgWaitForFinished(reply)); QVERIFY2(waitForFinish(reply) == Success, msgWaitForFinished(reply));