diff --git a/src/network/access/qdecompresshelper.cpp b/src/network/access/qdecompresshelper.cpp index 2534a14535b..a5e4a2b80eb 100644 --- a/src/network/access/qdecompresshelper.cpp +++ b/src/network/access/qdecompresshelper.cpp @@ -41,6 +41,7 @@ #include #include +#include #include #include @@ -131,13 +132,16 @@ bool QDecompressHelper::setEncoding(const QByteArray &encoding) Q_ASSERT(contentEncoding == QDecompressHelper::None); if (contentEncoding != QDecompressHelper::None) { qWarning("Encoding is already set."); + // This isn't an error, so it doesn't set errorStr, it's just wrong usage. return false; } ContentEncoding ce = encodingFromByteArray(encoding); if (ce == None) { - qWarning("An unsupported content encoding was selected: %s", encoding.data()); + errorStr = QCoreApplication::translate("QHttp", "Unsupported content encoding: %1") + .arg(QLatin1String(encoding)); return false; } + errorStr = QString(); // clear error return setEncoding(ce); } @@ -179,7 +183,8 @@ bool QDecompressHelper::setEncoding(ContentEncoding ce) break; } if (!decoderPointer) { - qWarning("Failed to initialize the decoder."); + errorStr = QCoreApplication::translate("QHttp", + "Failed to initialize the compression decoder."); contentEncoding = QDecompressHelper::None; return false; } @@ -386,8 +391,13 @@ qsizetype QDecompressHelper::read(char *data, qsizetype maxSize) uncompressedBytes -= bytesRead; totalUncompressedBytes += bytesRead; - if (isPotentialArchiveBomb()) + if (isPotentialArchiveBomb()) { + errorStr = QCoreApplication::translate( + "QHttp", + "The decompressed output exceeds the limits specified by " + "QNetworkRequest::decompressedSafetyCheckThreshold()"); return -1; + } return bytesRead; } @@ -458,11 +468,29 @@ qint64 QDecompressHelper::encodedBytesAvailable() const return compressedDataBuffer.byteAmount(); } +/*! + \internal + Returns whether or not the object is valid. + If it becomes invalid after an operation has been performed + then an error has occurred. + \sa errorString() +*/ bool QDecompressHelper::isValid() const { return contentEncoding != None; } +/*! + \internal + Returns a string describing the error that occurred or an empty + string if no error occurred. + \sa isValid() +*/ +QString QDecompressHelper::errorString() const +{ + return errorStr; +} + void QDecompressHelper::clear() { switch (contentEncoding) { @@ -504,6 +532,8 @@ void QDecompressHelper::clear() uncompressedBytes = 0; totalUncompressedBytes = 0; totalCompressedBytes = 0; + + errorStr.clear(); } qsizetype QDecompressHelper::readZLib(char *data, const qsizetype maxSize) @@ -659,8 +689,9 @@ qsizetype QDecompressHelper::readBrotli(char *data, const qsizetype maxSize) switch (result) { case BROTLI_DECODER_RESULT_ERROR: - qWarning("Brotli error: %s", - BrotliDecoderErrorString(BrotliDecoderGetErrorCode(brotliDecoderState))); + errorStr = QLatin1String("Brotli error: %1") + .arg(QString::fromUtf8(BrotliDecoderErrorString( + BrotliDecoderGetErrorCode(brotliDecoderState)))); return -1; case BROTLI_DECODER_RESULT_SUCCESS: BrotliDecoderDestroyInstance(brotliDecoderState); @@ -706,7 +737,8 @@ qsizetype QDecompressHelper::readZstandard(char *data, const qsizetype maxSize) while (outBuf.pos < outBuf.size && (inBuf.pos < inBuf.size || decoderHasData)) { size_t retValue = ZSTD_decompressStream(zstdStream, &outBuf, &inBuf); if (ZSTD_isError(retValue)) { - qWarning("ZStandard error: %s", ZSTD_getErrorName(retValue)); + errorStr = QLatin1String("ZStandard error: %1") + .arg(QString::fromUtf8(ZSTD_getErrorName(retValue))); return -1; } else { decoderHasData = false; diff --git a/src/network/access/qdecompresshelper_p.h b/src/network/access/qdecompresshelper_p.h index 4abca94de9f..859eeaca741 100644 --- a/src/network/access/qdecompresshelper_p.h +++ b/src/network/access/qdecompresshelper_p.h @@ -96,6 +96,8 @@ public: static bool isSupportedEncoding(const QByteArray &encoding); static QByteArrayList acceptedEncoding(); + QString errorString() const; + private: bool isPotentialArchiveBomb() const; @@ -117,6 +119,8 @@ private: std::unique_ptr countHelper; qint64 uncompressedBytes = 0; + QString errorStr; + // Used for calculating the ratio qint64 archiveBombCheckThreshold = 10 * 1024 * 1024; qint64 totalUncompressedBytes = 0; diff --git a/src/network/access/qhttp2protocolhandler.cpp b/src/network/access/qhttp2protocolhandler.cpp index 6b55b5ee1f6..f369133b8b5 100644 --- a/src/network/access/qhttp2protocolhandler.cpp +++ b/src/network/access/qhttp2protocolhandler.cpp @@ -1297,9 +1297,11 @@ void QHttp2ProtocolHandler::updateStream(Stream &stream, const Frame &frame, output.resize(read); replyPrivate->responseData.append(std::move(output)); } else if (read < 0) { - finishStreamWithError( - stream, QNetworkReply::ProtocolFailure, - QCoreApplication::translate("QHttp", "Data corrupted")); + const QString decompressError = + QCoreApplication::translate("QHttp", "Decompression failed: %1") + .arg(replyPrivate->decompressHelper.errorString()); + finishStreamWithError(stream, QNetworkReply::UnknownContentError, + decompressError); return; } } diff --git a/src/network/access/qhttpprotocolhandler.cpp b/src/network/access/qhttpprotocolhandler.cpp index 510cbb5e96f..f93531ea61e 100644 --- a/src/network/access/qhttpprotocolhandler.cpp +++ b/src/network/access/qhttpprotocolhandler.cpp @@ -203,7 +203,13 @@ void QHttpProtocolHandler::_q_receiveReply() } } else if (haveRead == -1) { // Some error occurred - m_connection->d_func()->emitReplyError(m_socket, m_reply, QNetworkReply::ProtocolFailure); + if (replyPrivate->autoDecompress && !replyPrivate->decompressHelper.isValid()) { + m_connection->d_func()->emitReplyError(m_socket, m_reply, + QNetworkReply::UnknownContentError); + } else { + m_connection->d_func()->emitReplyError(m_socket, m_reply, + QNetworkReply::ProtocolFailure); + } break; } } diff --git a/src/network/doc/src/qt6-changes.qdoc b/src/network/doc/src/qt6-changes.qdoc index b7881e3cc22..13e7017a342 100644 --- a/src/network/doc/src/qt6-changes.qdoc +++ b/src/network/doc/src/qt6-changes.qdoc @@ -193,4 +193,14 @@ \code request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false); \endcode + + \section2 QNetworkAccessManager now guards against archive bombs + + Starting with Qt 6.2 QNetworkAccessManager will guard against compressed + files that decompress to files which are much larger than their compressed + form by erroring out the reply if the decompression ratio exceeds a certain + threshold. + This check is only applied to files larger than a certain size, which can be + customized (or disabled by passing -1) by calling + \l{QNetworkRequest::setDecompressedSafetyCheckThreshold()}. */ diff --git a/tests/auto/network/access/qdecompresshelper/tst_qdecompresshelper.cpp b/tests/auto/network/access/qdecompresshelper/tst_qdecompresshelper.cpp index c5c41b6f4fd..3cc00205c74 100644 --- a/tests/auto/network/access/qdecompresshelper/tst_qdecompresshelper.cpp +++ b/tests/auto/network/access/qdecompresshelper/tst_qdecompresshelper.cpp @@ -424,10 +424,13 @@ void tst_QDecompressHelper::archiveBomb() QVERIFY(bytesRead <= output.size()); QVERIFY(helper.isValid()); - if (shouldFail) + if (shouldFail) { QCOMPARE(bytesRead, -1); - else + QVERIFY(!helper.errorString().isEmpty()); + } else { QVERIFY(bytesRead > 0); + QVERIFY(helper.errorString().isEmpty()); + } } void tst_QDecompressHelper::bigZlib() diff --git a/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp b/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp index 5d479f080f6..550284c9a0e 100644 --- a/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp +++ b/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp @@ -7077,7 +7077,7 @@ void tst_QNetworkReply::compressedHttpReplyBrokenGzip() QCOMPARE(waitForFinish(reply), int(Failure)); - QCOMPARE(reply->error(), QNetworkReply::ProtocolFailure); + QCOMPARE(reply->error(), QNetworkReply::UnknownContentError); } // TODO add similar test for FTP