From c501e35d9905df6390c03a97595772773428e169 Mon Sep 17 00:00:00 2001 From: Mate Barany Date: Thu, 24 Sep 2026 17:05:25 +0200 Subject: [PATCH] QHttpNetworkReply: limit header block size while reading readHeader() accumulated the response header block into 'fragment' one byte at a time without any size check. The total header size limit was only enforced by QHttpHeaderParser::parseHeaders(), which runs after the end of the header block has been seen. A server that never terminates the header block could therefore make the client grow 'fragment' without bound and exhaust memory. Enforce the parser's configured total header size limit while accumulating, allowing for the terminating line endings which the parser does not count, so that no block the parser would accept is rejected early. Fixes: QTBUG-150779 Change-Id: I5a26c02ef3b944e40b5d68fdfcb42ed4756140fc Reviewed-by: Lena Biliaieva (cherry picked from commit 89bceafa2016e7e72fc90e0aab74fbc3a5872df2) Reviewed-by: Qt Cherry-pick Bot (cherry picked from commit aac1d303c0329241ac59c0c23cb5e4b82251fcfa) Reviewed-by: Mate Barany --- diff --git a/src/network/access/qhttpheaderparser_p.h b/src/network/access/qhttpheaderparser_p.h index 777b33a..f2cdb0f 100644 --- a/src/network/access/qhttpheaderparser_p.h +++ b/src/network/access/qhttpheaderparser_p.h @@ -89,6 +89,14 @@ } qsizetype maxTotalHeaderSize() const { return maxTotalSize; } + qsizetype maxRawHeaderSize() const { + constexpr qsizetype TrailingLineEndings = 4; + qsizetype result; + if (qAddOverflow(maxTotalSize, TrailingLineEndings, &result)) + return (std::numeric_limits::max)(); + return result; + } + void setMaxHeaderFields(qsizetype size) { Q_ASSERT(size > 0); maxFieldCount = size; diff --git a/src/network/access/qhttpnetworkreply.cpp b/src/network/access/qhttpnetworkreply.cpp index 762b500..159fb50 100644 --- a/src/network/access/qhttpnetworkreply.cpp +++ b/src/network/access/qhttpnetworkreply.cpp @@ -458,6 +458,12 @@ fragment.append(c); bytes++; + // Enforce the total header size limit while accumulating. + if (fragment.size() > parser.maxRawHeaderSize()) { + fragment.clear(); + return -1; + } + if (c == '\n') { // check for possible header endings. As per HTTP rfc, // the header endings will be marked by CRLFCRLF. But diff --git a/tests/auto/network/access/qhttpnetworkreply/tst_qhttpnetworkreply.cpp b/tests/auto/network/access/qhttpnetworkreply/tst_qhttpnetworkreply.cpp index b5c79b9..5ef3e7f 100644 --- a/tests/auto/network/access/qhttpnetworkreply/tst_qhttpnetworkreply.cpp +++ b/tests/auto/network/access/qhttpnetworkreply/tst_qhttpnetworkreply.cpp @@ -21,6 +21,9 @@ void parseEndOfHeader_data(); void parseEndOfHeader(); + + void headerSizeLimit_data(); + void headerSizeLimit(); }; void tst_QHttpNetworkReply::parseHeader_data() @@ -232,5 +235,50 @@ QCOMPARE(headerBytes, lengths); } +void tst_QHttpNetworkReply::headerSizeLimit_data() +{ + QTest::addColumn("headers"); + QTest::addColumn("accepted"); + + // Limit used below is 64 bytes of header content. + const QByteArray field = "X-Field: " + QByteArray(40, 'a'); // 49 bytes + + QTest::newRow("empty") << QByteArray("\r\n") << true; + QTest::newRow("within-limit") << QByteArray(field + "\r\n\r\n") << true; + QTest::newRow("within-limit-LFLF") << QByteArray(field + "\n\n") << true; + QTest::newRow("at-limit") << QByteArray("X-Field: " + QByteArray(55, 'a') + "\r\n\r\n") + << true; + QTest::newRow("over-limit-terminated") + << QByteArray(field + "\r\n" + field + "\r\n\r\n") << false; + QTest::newRow("over-limit-unterminated") << QByteArray("X-Field: " + QByteArray(4096, 'a')) + << false; + QTest::newRow("over-limit-unterminated-lines") + << QByteArray(field + "\r\n").repeated(100) << false; +} + +void tst_QHttpNetworkReply::headerSizeLimit() +{ + QFETCH(QByteArray, headers); + QFETCH(bool, accepted); + + TestHeaderSocket socket(headers); + TestHeaderReply reply; + QHttpNetworkReplyPrivate *replyPrivate = reply.replyPrivate(); + replyPrivate->parser.setMaxTotalHeaderSize(64); + + const qint64 headerBytes = replyPrivate->readHeader(&socket); + if (accepted) { + QCOMPARE(headerBytes, qint64(headers.size())); + QCOMPARE(replyPrivate->state, QHttpNetworkReplyPrivate::ReadingDataState); + } else { + QCOMPARE(headerBytes, qint64(-1)); + QCOMPARE_NE(replyPrivate->state, QHttpNetworkReplyPrivate::ReadingDataState); + // Nothing may be retained from the rejected block. + QVERIFY(replyPrivate->fragment.isEmpty()); + // Must stop reading as soon as the limit is exceeded, not consume everything. + QCOMPARE_LE(socket.inputBuffer.pos(), replyPrivate->parser.maxRawHeaderSize() + 1); + } +} + QTEST_MAIN(tst_QHttpNetworkReply) #include "tst_qhttpnetworkreply.moc"