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:
parent
347310eb21
commit
69982182a3
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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)) {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
Loading…
Reference in New Issue
Block a user