https://bugs.gentoo.org/986097 --- a/src/network/access/qhttpheaderparser_p.h +++ b/src/network/access/qhttpheaderparser_p.h @@ -90,4 +90,12 @@ 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); --- a/src/network/access/qhttpnetworkreply.cpp +++ b/src/network/access/qhttpnetworkreply.cpp @@ -424,4 +424,12 @@ return -1; } + + // parseStatus() rejects reason phrases longer than MAX_REASON_PHRASE_SIZE anyway, + // so anything beyond that plus a generous allowance for the rest of the line + // can never be valid. + if (fragment.size() > HeaderConstants::MAX_REASON_PHRASE_SIZE + 64) { + fragment.clear(); + return -1; + } } while (haveRead == 1); @@ -459,4 +467,10 @@ 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, --- a/tests/auto/network/access/qhttpnetworkreply/tst_qhttpnetworkreply.cpp +++ b/tests/auto/network/access/qhttpnetworkreply/tst_qhttpnetworkreply.cpp @@ -22,4 +22,9 @@ void parseEndOfHeader_data(); void parseEndOfHeader(); + + void headerSizeLimit_data(); + void headerSizeLimit(); + + void statusLineSizeLimit(); }; @@ -233,4 +238,70 @@ } +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); + } +} + +void tst_QHttpNetworkReply::statusLineSizeLimit() +{ + // A status line that is never terminated must be rejected once it can + // no longer be valid, instead of being buffered indefinitely. + const QByteArray status = "HTTP/1.1 200 " + QByteArray(1024 * 1024, 'a'); + TestHeaderSocket socket(status); + TestHeaderReply reply; + QHttpNetworkReplyPrivate *replyPrivate = reply.replyPrivate(); + + QCOMPARE(replyPrivate->readStatus(&socket), qint64(-1)); + QVERIFY(replyPrivate->fragment.isEmpty()); + QCOMPARE_LT(socket.inputBuffer.pos(), 4096); + + // The longest acceptable reason phrase still parses. + const QByteArray okStatus = "HTTP/1.1 200 " + QByteArray(1024, 'a') + "\r\n"; + TestHeaderSocket okSocket(okStatus); + TestHeaderReply okReply; + QCOMPARE(okReply.replyPrivate()->readStatus(&okSocket), qint64(okStatus.size())); + QCOMPARE(okReply.statusCode(), 200); +} + QTEST_MAIN(tst_QHttpNetworkReply) #include "tst_qhttpnetworkreply.moc"