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/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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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)) {
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user