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>bb10
parent
16a1ddd733
commit
a07f35409b
|
|
@ -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,9 +383,59 @@ 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
Loading…
Reference in New Issue