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/qiodevice.h>
#include <limits>
#include <zlib.h>
#if QT_CONFIG(brotli)
@ -328,7 +329,7 @@ bool QDecompressHelper::countInternal(const QByteArray &data)
if (countDecompressed) {
if (!countHelper) {
countHelper = std::make_unique<QDecompressHelper>();
countHelper->setArchiveBombDetectionEnabled(archiveBombDetectionEnabled);
countHelper->setMinimumArchiveBombSize(minimumArchiveBombSize);
countHelper->setEncoding(contentEncoding);
}
countHelper->feed(data);
@ -346,7 +347,7 @@ bool QDecompressHelper::countInternal(const QByteDataBuffer &buffer)
if (countDecompressed) {
if (!countHelper) {
countHelper = std::make_unique<QDecompressHelper>();
countHelper->setArchiveBombDetectionEnabled(archiveBombDetectionEnabled);
countHelper->setMinimumArchiveBombSize(minimumArchiveBombSize);
countHelper->setEncoding(contentEncoding);
}
countHelper->feed(buffer);
@ -393,28 +394,19 @@ qsizetype QDecompressHelper::read(char *data, qsizetype maxSize)
/*!
\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.
Set the \a threshold required before the archive bomb detection kicks in.
By default this is 10MB. Setting it to -1 is treated as disabling the
feature.
*/
void QDecompressHelper::setArchiveBombDetectionEnabled(bool enable)
{
archiveBombDetectionEnabled = enable;
if (countHelper)
countHelper->setArchiveBombDetectionEnabled(enable);
}
void QDecompressHelper::setMinimumArchiveBombSize(qint64 threshold)
{
if (threshold == -1)
threshold = std::numeric_limits<qint64>::max();
minimumArchiveBombSize = threshold;
}
bool QDecompressHelper::isPotentialArchiveBomb() const
{
if (!archiveBombDetectionEnabled)
return false;
if (totalCompressedBytes == 0)
return false;
@ -430,12 +422,16 @@ bool QDecompressHelper::isPotentialArchiveBomb() const
break;
case Deflate:
case GZip:
// This value is mentioned in docs for
// QNetworkRequest::setMinimumArchiveBombSize, keep synchronized
if (ratio > 40) {
return true;
}
break;
case Brotli:
case Zstandard:
// This value is mentioned in docs for
// QNetworkRequest::setMinimumArchiveBombSize, keep synchronized
if (ratio > 100) {
return true;
}

View File

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

View File

@ -1237,8 +1237,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);
httpReplyPrivate->decompressHelper.setMinimumArchiveBombSize(
httpReplyPrivate->request.minimumArchiveBombSize());
}
if (QHttpNetworkReply::isHttpRedirect(statusCode)) {

View File

@ -557,8 +557,7 @@ 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);
decompressHelper.setMinimumArchiveBombSize(request.minimumArchiveBombSize());
}
}
return bytes;

View File

@ -57,6 +57,7 @@ QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate(const QHttpNetworkRequest
customVerb(other.customVerb),
priority(other.priority),
uploadByteDevice(other.uploadByteDevice),
minimumArchiveBombSize(other.minimumArchiveBombSize),
autoDecompress(other.autoDecompress),
pipeliningAllowed(other.pipeliningAllowed),
http2Allowed(other.http2Allowed),
@ -64,7 +65,6 @@ QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate(const QHttpNetworkRequest
withCredentials(other.withCredentials),
ssl(other.ssl),
preConnect(other.preConnect),
ignoreDecompressionRatio(other.ignoreDecompressionRatio),
needResendWithCredentials(other.needResendWithCredentials),
redirectCount(other.redirectCount),
redirectPolicy(other.redirectPolicy),
@ -93,7 +93,8 @@ bool QHttpNetworkRequestPrivate::operator==(const QHttpNetworkRequestPrivate &ot
&& (preConnect == other.preConnect)
&& (redirectPolicy == other.redirectPolicy)
&& (peerVerifyName == other.peerVerifyName)
&& (needResendWithCredentials == other.needResendWithCredentials);
&& (needResendWithCredentials == other.needResendWithCredentials)
&& (minimumArchiveBombSize == other.minimumArchiveBombSize);
}
QByteArray QHttpNetworkRequest::methodName() const
@ -405,14 +406,14 @@ void QHttpNetworkRequest::setPeerVerifyName(const QString &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

View File

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

View File

@ -774,14 +774,7 @@ 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.setMinimumArchiveBombSize(newHttpRequest.minimumArchiveBombSize());
httpRequest.setPeerVerifyName(newHttpRequest.peerVerifyName());
// Create the HTTP thread delegate

View File

@ -441,6 +441,7 @@ public:
peerVerifyName = other.peerVerifyName;
#if QT_CONFIG(http)
h2Configuration = other.h2Configuration;
minimumArchiveBombSize = other.minimumArchiveBombSize;
#endif
transferTimeout = other.transferTimeout;
}
@ -455,6 +456,7 @@ public:
peerVerifyName == other.peerVerifyName
#if QT_CONFIG(http)
&& h2Configuration == other.h2Configuration
&& minimumArchiveBombSize == other.minimumArchiveBombSize
#endif
&& transferTimeout == other.transferTimeout
;
@ -470,6 +472,7 @@ public:
QString peerVerifyName;
#if QT_CONFIG(http)
QHttp2Configuration h2Configuration;
qint64 minimumArchiveBombSize = 10ll * 1024ll * 1024ll;
#endif
int transferTimeout;
};
@ -896,7 +899,50 @@ void QNetworkRequest::setHttp2Configuration(const QHttp2Configuration &configura
{
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)
#if QT_CONFIG(http) || defined(Q_CLANG_QDOC) || defined (Q_OS_WASM)
/*!
\since 5.15

View File

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

View File

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

View File

@ -7044,8 +7044,7 @@ void tst_QNetworkReply::qtbug12908compressedHttpReply()
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"));
request.setMinimumArchiveBombSize(-1);
QNetworkReplyPtr reply(manager.get(request));
QVERIFY2(waitForFinish(reply) == Success, msgWaitForFinished(reply));