From 853f0a2a45722d7d76dcb5062e455f3f3bc8d48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubo=C5=A1=20Uhliarik?= Date: Tue, 19 Mar 2024 16:52:20 +0100 Subject: [PATCH] Resolves: RHEL-28529 - squid:4/squid: Denial of Service in HTTP Chunked Decoding (CVE-2024-25111) Resolves: RHEL-26088 - squid:4/squid: denial of service in HTTP header parser (CVE-2024-25617) --- squid-4.15-CVE-2024-25111.patch | 193 ++++++++++++++++++++++++++++++++ squid-4.15-CVE-2024-25617.patch | 105 +++++++++++++++++ squid.spec | 14 ++- 3 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 squid-4.15-CVE-2024-25111.patch create mode 100644 squid-4.15-CVE-2024-25617.patch diff --git a/squid-4.15-CVE-2024-25111.patch b/squid-4.15-CVE-2024-25111.patch new file mode 100644 index 0000000..e8ea010 --- /dev/null +++ b/squid-4.15-CVE-2024-25111.patch @@ -0,0 +1,193 @@ +diff --git a/src/http.cc b/src/http.cc +index b006300..023e411 100644 +--- a/src/http.cc ++++ b/src/http.cc +@@ -52,6 +52,7 @@ + #include "rfc1738.h" + #include "SquidConfig.h" + #include "SquidTime.h" ++#include "SquidMath.h" + #include "StatCounters.h" + #include "Store.h" + #include "StrList.h" +@@ -1150,18 +1151,26 @@ HttpStateData::readReply(const CommIoCbParams &io) + * Plus, it breaks our lame *HalfClosed() detection + */ + +- Must(maybeMakeSpaceAvailable(true)); +- CommIoCbParams rd(this); // will be expanded with ReadNow results +- rd.conn = io.conn; +- rd.size = entry->bytesWanted(Range(0, inBuf.spaceSize())); ++ size_t moreDataPermission = 0; ++ if ((!canBufferMoreReplyBytes(&moreDataPermission) || !moreDataPermission)) { ++ abortTransaction("ready to read required data, but the read buffer is full and cannot be drained"); ++ return; ++ } ++ ++ const auto readSizeMax = maybeMakeSpaceAvailable(moreDataPermission); ++ // TODO: Move this logic inside maybeMakeSpaceAvailable(): ++ const auto readSizeWanted = readSizeMax ? entry->bytesWanted(Range(0, readSizeMax)) : 0; + +- if (rd.size <= 0) { ++ if (readSizeWanted <= 0) { + assert(entry->mem_obj); + AsyncCall::Pointer nilCall; + entry->mem_obj->delayRead(DeferredRead(readDelayed, this, CommRead(io.conn, NULL, 0, nilCall))); + return; + } + ++ CommIoCbParams rd(this); // will be expanded with ReadNow results ++ rd.conn = io.conn; ++ rd.size = readSizeWanted; + switch (Comm::ReadNow(rd, inBuf)) { + case Comm::INPROGRESS: + if (inBuf.isEmpty()) +@@ -1520,8 +1529,11 @@ HttpStateData::maybeReadVirginBody() + if (!Comm::IsConnOpen(serverConnection) || fd_table[serverConnection->fd].closing()) + return; + +- if (!maybeMakeSpaceAvailable(false)) ++ size_t moreDataPermission = 0; ++ if ((!canBufferMoreReplyBytes(&moreDataPermission)) || !moreDataPermission) { ++ abortTransaction("more response bytes required, but the read buffer is full and cannot be drained"); + return; ++ } + + // XXX: get rid of the do_next_read flag + // check for the proper reasons preventing read(2) +@@ -1539,40 +1551,79 @@ HttpStateData::maybeReadVirginBody() + Comm::Read(serverConnection, call); + } + ++/// Desired inBuf capacity based on various capacity preferences/limits: ++/// * a smaller buffer may not hold enough for look-ahead header/body parsers; ++/// * a smaller buffer may result in inefficient tiny network reads; ++/// * a bigger buffer may waste memory; ++/// * a bigger buffer may exceed SBuf storage capabilities (SBuf::maxSize); ++size_t ++HttpStateData::calcReadBufferCapacityLimit() const ++{ ++ if (!flags.headers_parsed) ++ return Config.maxReplyHeaderSize; ++ ++ // XXX: Our inBuf is not used to maintain the read-ahead gap, and using ++ // Config.readAheadGap like this creates huge read buffers for large ++ // read_ahead_gap values. TODO: Switch to using tcp_recv_bufsize as the ++ // primary read buffer capacity factor. ++ // ++ // TODO: Cannot reuse throwing NaturalCast() here. Consider removing ++ // .value() dereference in NaturalCast() or add/use NaturalCastOrMax(). ++ const auto configurationPreferences = NaturalSum(Config.readAheadGap).second ? NaturalSum(Config.readAheadGap).first : SBuf::maxSize; ++ ++ // TODO: Honor TeChunkedParser look-ahead and trailer parsing requirements ++ // (when explicit configurationPreferences are set too low). ++ ++ return std::min(configurationPreferences, SBuf::maxSize); ++} ++ ++/// The maximum number of virgin reply bytes we may buffer before we violate ++/// the currently configured response buffering limits. ++/// \retval std::nullopt means that no more virgin response bytes can be read ++/// \retval 0 means that more virgin response bytes may be read later ++/// \retval >0 is the number of bytes that can be read now (subject to other constraints) + bool +-HttpStateData::maybeMakeSpaceAvailable(bool doGrow) ++HttpStateData::canBufferMoreReplyBytes(size_t *maxReadSize) const + { +- // how much we are allowed to buffer +- const int limitBuffer = (flags.headers_parsed ? Config.readAheadGap : Config.maxReplyHeaderSize); +- +- if (limitBuffer < 0 || inBuf.length() >= (SBuf::size_type)limitBuffer) { +- // when buffer is at or over limit already +- debugs(11, 7, "will not read up to " << limitBuffer << ". buffer has (" << inBuf.length() << "/" << inBuf.spaceSize() << ") from " << serverConnection); +- debugs(11, DBG_DATA, "buffer has {" << inBuf << "}"); +- // Process next response from buffer +- processReply(); +- return false; ++#if USE_ADAPTATION ++ // If we do not check this now, we may say the final "no" prematurely below ++ // because inBuf.length() will decrease as adaptation drains buffered bytes. ++ if (responseBodyBuffer) { ++ debugs(11, 3, "yes, but waiting for adaptation to drain read buffer"); ++ *maxReadSize = 0; // yes, we may be able to buffer more (but later) ++ return true; ++ } ++#endif ++ ++ const auto maxCapacity = calcReadBufferCapacityLimit(); ++ if (inBuf.length() >= maxCapacity) { ++ debugs(11, 3, "no, due to a full buffer: " << inBuf.length() << '/' << inBuf.spaceSize() << "; limit: " << maxCapacity); ++ return false; // no, configuration prohibits buffering more + } + ++ *maxReadSize = (maxCapacity - inBuf.length()); // positive ++ debugs(11, 7, "yes, may read up to " << *maxReadSize << " into " << inBuf.length() << '/' << inBuf.spaceSize()); ++ return true; // yes, can read up to this many bytes (subject to other constraints) ++} ++ ++/// prepare read buffer for reading ++/// \return the maximum number of bytes the caller should attempt to read ++/// \retval 0 means that the caller should delay reading ++size_t ++HttpStateData::maybeMakeSpaceAvailable(const size_t maxReadSize) ++{ + // how much we want to read +- const size_t read_size = calcBufferSpaceToReserve(inBuf.spaceSize(), (limitBuffer - inBuf.length())); ++ const size_t read_size = calcBufferSpaceToReserve(inBuf.spaceSize(), maxReadSize); + +- if (!read_size) { ++ if (read_size < 2) { + debugs(11, 7, "will not read up to " << read_size << " into buffer (" << inBuf.length() << "/" << inBuf.spaceSize() << ") from " << serverConnection); +- return false; ++ return 0; + } + +- // just report whether we could grow or not, do not actually do it +- if (doGrow) +- return (read_size >= 2); +- + // we may need to grow the buffer + inBuf.reserveSpace(read_size); +- debugs(11, 8, (!flags.do_next_read ? "will not" : "may") << +- " read up to " << read_size << " bytes info buf(" << inBuf.length() << "/" << inBuf.spaceSize() << +- ") from " << serverConnection); +- +- return (inBuf.spaceSize() >= 2); // only read if there is 1+ bytes of space available ++ debugs(11, 7, "may read up to " << read_size << " bytes info buffer (" << inBuf.length() << "/" << inBuf.spaceSize() << ") from " << serverConnection); ++ return read_size; + } + + /// called after writing the very last request byte (body, last-chunk, etc) +diff --git a/src/http.h b/src/http.h +index 8965b77..007d2e6 100644 +--- a/src/http.h ++++ b/src/http.h +@@ -15,6 +15,8 @@ + #include "http/StateFlags.h" + #include "sbuf/SBuf.h" + ++#include ++ + class FwdState; + class HttpHeader; + +@@ -107,16 +109,9 @@ private: + + void abortTransaction(const char *reason) { abortAll(reason); } // abnormal termination + +- /** +- * determine if read buffer can have space made available +- * for a read. +- * +- * \param grow whether to actually expand the buffer +- * +- * \return whether the buffer can be grown to provide space +- * regardless of whether the grow actually happened. +- */ +- bool maybeMakeSpaceAvailable(bool grow); ++ size_t calcReadBufferCapacityLimit() const; ++ bool canBufferMoreReplyBytes(size_t *maxReadSize) const; ++ size_t maybeMakeSpaceAvailable(size_t maxReadSize); + + // consuming request body + virtual void handleMoreRequestBodyAvailable(); diff --git a/squid-4.15-CVE-2024-25617.patch b/squid-4.15-CVE-2024-25617.patch new file mode 100644 index 0000000..86e391a --- /dev/null +++ b/squid-4.15-CVE-2024-25617.patch @@ -0,0 +1,105 @@ +diff --git a/src/SquidString.h b/src/SquidString.h +index a791885..b9aef38 100644 +--- a/src/SquidString.h ++++ b/src/SquidString.h +@@ -114,7 +114,16 @@ private: + + size_type len_; /* current length */ + +- static const size_type SizeMax_ = 65535; ///< 64K limit protects some fixed-size buffers ++ /// An earlier 64KB limit was meant to protect some fixed-size buffers, but ++ /// (a) we do not know where those buffers are (or whether they still exist) ++ /// (b) too many String users unknowingly exceeded that limit and asserted. ++ /// We are now using a larger limit to reduce the number of (b) cases, ++ /// especially cases where "compact" lists of items grow 50% in size when we ++ /// convert them to canonical form. The new limit is selected to withstand ++ /// concatenation and ~50% expansion of two HTTP headers limited by default ++ /// request_header_max_size and reply_header_max_size settings. ++ static const size_type SizeMax_ = 3*64*1024 - 1; ++ + /// returns true after increasing the first argument by extra if the sum does not exceed SizeMax_ + static bool SafeAdd(size_type &base, size_type extra) { if (extra <= SizeMax_ && base <= SizeMax_ - extra) { base += extra; return true; } return false; } + +diff --git a/src/cache_cf.cc b/src/cache_cf.cc +index a9c1b7e..46f07bb 100644 +--- a/src/cache_cf.cc ++++ b/src/cache_cf.cc +@@ -935,6 +935,18 @@ configDoConfigure(void) + (uint32_t)Config.maxRequestBufferSize, (uint32_t)Config.maxRequestHeaderSize); + } + ++ // Warn about the dangers of exceeding String limits when manipulating HTTP ++ // headers. Technically, we do not concatenate _requests_, so we could relax ++ // their check, but we keep the two checks the same for simplicity sake. ++ const auto safeRawHeaderValueSizeMax = (String::SizeMaxXXX()+1)/3; ++ // TODO: static_assert(safeRawHeaderValueSizeMax >= 64*1024); // no WARNINGs for default settings ++ if (Config.maxRequestHeaderSize > safeRawHeaderValueSizeMax) ++ debugs(3, DBG_CRITICAL, "WARNING: Increasing request_header_max_size beyond " << safeRawHeaderValueSizeMax << ++ " bytes makes Squid more vulnerable to denial-of-service attacks; configured value: " << Config.maxRequestHeaderSize << " bytes"); ++ if (Config.maxReplyHeaderSize > safeRawHeaderValueSizeMax) ++ debugs(3, DBG_CRITICAL, "WARNING: Increasing reply_header_max_size beyond " << safeRawHeaderValueSizeMax << ++ " bytes makes Squid more vulnerable to denial-of-service attacks; configured value: " << Config.maxReplyHeaderSize << " bytes"); ++ + /* + * Disable client side request pipelining if client_persistent_connections OFF. + * Waste of resources queueing any pipelined requests when the first will close the connection. +diff --git a/src/cf.data.pre b/src/cf.data.pre +index bc2ddcd..d55b870 100644 +--- a/src/cf.data.pre ++++ b/src/cf.data.pre +@@ -6196,11 +6196,14 @@ TYPE: b_size_t + DEFAULT: 64 KB + LOC: Config.maxRequestHeaderSize + DOC_START +- This specifies the maximum size for HTTP headers in a request. +- Request headers are usually relatively small (about 512 bytes). +- Placing a limit on the request header size will catch certain +- bugs (for example with persistent connections) and possibly +- buffer-overflow or denial-of-service attacks. ++ This directives limits the header size of a received HTTP request ++ (including request-line). Increasing this limit beyond its 64 KB default ++ exposes certain old Squid code to various denial-of-service attacks. This ++ limit also applies to received FTP commands. ++ ++ This limit has no direct affect on Squid memory consumption. ++ ++ Squid does not check this limit when sending requests. + DOC_END + + NAME: reply_header_max_size +@@ -6209,11 +6212,14 @@ TYPE: b_size_t + DEFAULT: 64 KB + LOC: Config.maxReplyHeaderSize + DOC_START +- This specifies the maximum size for HTTP headers in a reply. +- Reply headers are usually relatively small (about 512 bytes). +- Placing a limit on the reply header size will catch certain +- bugs (for example with persistent connections) and possibly +- buffer-overflow or denial-of-service attacks. ++ This directives limits the header size of a received HTTP response ++ (including status-line). Increasing this limit beyond its 64 KB default ++ exposes certain old Squid code to various denial-of-service attacks. This ++ limit also applies to FTP command responses. ++ ++ Squid also checks this limit when loading hit responses from disk cache. ++ ++ Squid does not check this limit when sending responses. + DOC_END + + NAME: request_body_max_size +diff --git a/src/http.cc b/src/http.cc +index 877172d..b006300 100644 +--- a/src/http.cc ++++ b/src/http.cc +@@ -1820,8 +1820,9 @@ HttpStateData::httpBuildRequestHeader(HttpRequest * request, + + String strFwd = hdr_in->getList(Http::HdrType::X_FORWARDED_FOR); + +- // if we cannot double strFwd size, then it grew past 50% of the limit +- if (!strFwd.canGrowBy(strFwd.size())) { ++ // Detect unreasonably long header values. And paranoidly check String ++ // limits: a String ought to accommodate two reasonable-length values. ++ if (strFwd.size() > 32*1024 || !strFwd.canGrowBy(strFwd.size())) { + // There is probably a forwarding loop with Via detection disabled. + // If we do nothing, String will assert on overflow soon. + // TODO: Terminate all transactions with huge XFF? diff --git a/squid.spec b/squid.spec index 3df353f..a936bf2 100644 --- a/squid.spec +++ b/squid.spec @@ -2,7 +2,7 @@ Name: squid Version: 4.15 -Release: 9%{?dist} +Release: 10%{?dist} Summary: The Squid proxy caching server Epoch: 7 # See CREDITS for breakdown of non GPLv2+ code @@ -65,6 +65,10 @@ Patch308: squid-4.15-CVE-2023-49285.patch Patch309: squid-4.15-CVE-2023-49286.patch # https://bugzilla.redhat.com/show_bug.cgi?id=2254663 Patch310: squid-4.15-CVE-2023-50269.patch +# https://bugzilla.redhat.com/show_bug.cgi?id=2264309 +Patch311: squid-4.15-CVE-2024-25617.patch +# https://bugzilla.redhat.com/show_bug.cgi?id=2268366 +Patch312: squid-4.15-CVE-2024-25111.patch Requires: bash >= 2.0 @@ -141,6 +145,8 @@ lookup program (dnsserver), a program for retrieving FTP data %patch308 -p1 -b .CVE-2023-49285 %patch309 -p1 -b .CVE-2023-49286 %patch310 -p1 -b .CVE-2023-50269 +%patch311 -p1 -b .CVE-2024-25617 +%patch312 -p1 -b .CVE-2024-25111 # https://bugzilla.redhat.com/show_bug.cgi?id=1679526 @@ -358,6 +364,12 @@ fi %changelog +* Tue Mar 19 2024 Luboš Uhliarik - 7:4.15-10 +- Resolves: RHEL-28529 - squid:4/squid: Denial of Service in HTTP Chunked + Decoding (CVE-2024-25111) +- Resolves: RHEL-26088 - squid:4/squid: denial of service in HTTP header + parser (CVE-2024-25617) + * Fri Feb 02 2024 Luboš Uhliarik - 7:4.15-9 - Resolves: RHEL-19552 - squid:4/squid: denial of service in HTTP request parsing (CVE-2023-50269)