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
Mårten Nordheim 2020-08-19 12:47:47 +02:00
parent 16a1ddd733
commit a07f35409b
9 changed files with 99 additions and 0 deletions

View File

@ -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)

View File

@ -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;

View File

@ -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)

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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);

View File

@ -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));