| الوصف | I identified a remotely reachable out-of-bounds read vulnerability in CIPster master in the TCP encapsulation receive path. The issue was publicly documented by me in GitHub issue #46 and reproduced on commit 1802525be27d33e19a9a83c163e331a1d13b1892. The maintainer later closed the issue as completed in commit e8e9dba. In my assessment, this is a real security defect rather than a mere robustness bug, because a remote peer can drive the server into parsing data outside the valid bounds of the global TCP receive buffer and terminate the process under AddressSanitizer.
The defect is located in the interaction between Encapsulation::ReceiveTcpMsg(), HandleDataOnTcpSocket(), the global TCP receive buffer s_buf in source/src/enet_encap/networkhandler.cc, and the CPF parsing logic in source/src/enet_encap/cpf.cc. The vulnerable behavior appears when a TCP encapsulation packet declares a payload length that exceeds the available capacity of the receive buffer supplied to Encapsulation::ReceiveTcpMsg(). Instead of treating that condition as a hard failure for the current message, the old code drained and discarded the oversized body from the socket but still returned remaining + ENCAPSULATION_HEADER_LENGTH, effectively reporting the packet as if the full encapsulation message had been stored successfully in memory. That return value was then trusted by HandleDataOnTcpSocket(), which created a BufReader over s_buf using the reported logical message length, even though only the first 24-byte encapsulation header of the new packet had actually been written into the buffer.
Because the oversized body had been discarded rather than stored, the memory region after the new header in s_buf was not populated with the current message body. Instead, the tail of s_buf still contained stale bytes left over from a previous request processed on the same TCP session. This created a parsing state confusion bug: if a prior request had primed s_buf[24..] with attacker-controlled bytes that resembled a valid Common Packet Format layout, and the next request was an oversized SendRRData message, the parser would treat the stale bytes as the current CPF body. Once that happened, Cpf::DeserializeCpf() continued reading item headers and fields through BufReader using a logical length that no longer matched the real bytes belonging to the current packet. As I demonstrated in the report, the logical cursor eventually ran beyond the actual s_buf object and reached the right redzone, resulting in a global-buffer-overflow read in BufReader::get16() in source/src/byte_bufs.impl.
The root cause is a semantic inconsistency between the receive layer and the parse layer. The receive layer consumed the oversized body from the network stream and discarded it, but still signaled “complete packet available” to the parser. The parser, which had no way to know that the body had been dropped, trusted the reported size and proceeded to interpret whatever bytes followed the freshly received header in the static receive buffer. In other words, the implementation violated the invariant that the reported message length must correspond to bytes actually resident in the provided buffer. Once that invariant was broken, stale-buffer reparse became possible, and the parser crossed the valid object boundary.
My report provided a full crash trace captured with AddressSanitizer. The observed fault was a READ of size 1 in BufReader::get16(), called from Cpf::DeserializeCpf(), then Cpf::NotifyCommonPacketFormat(), Encapsulation::HandleReceivedExplicitTcpData(), HandleDataOnTcpSocket(), NetworkHandlerProcessOnce(), and finally the POSIX example main routine. AddressSanitizer reported that the invalid access occurred exactly 0 bytes to the right of global variable s_buf, which is defined in source/src/enet_encap/networkhandler.cc and has size 1800 bytes. This proves the read crossed the actual object boundary of the global receive buffer rather than merely consuming semantically invalid but still in-bounds data. The result was process abortion under ASan, and the same defect represents a remotely triggerable denial of service because malformed traffic can force the server into an invalid parse path and terminate service availability.
The triggering conditions I documented are concrete and reproducible. The attack does not depend on local access, source modification, or undefined test harness behavior. It requires a single TCP session to the server. First, the attacker sends a request that leaves a crafted CPF-like layout in the global receive buffer tail. Next, on the same session, the attacker sends an oversized SendRRData packet whose encapsulation header is received into s_buf but whose body exceeds the available receive-buffer capacity. The body is drained and dropped, yet the old implementation reports the packet length as if the whole message had been received. HandleDataOnTcpSocket() then constructs BufReader(s_buf, num_read) using that incorrect length, and the stale bytes from the prior request are reparsed as the current CPF payload. The parser continues reading based on the inflated logical boundary until it hits memory beyond s_buf.
From a security perspective, the demonstrated impact is remote denial of service. In my testing, the process aborted when the out-of-bounds read reached the global redzone. I am intentionally not overstating the impact beyond what I verified. The public evidence I provided establishes a reproducible out-of-bounds read and service crash. That is sufficient for security classification and CVE assignment, especially because the bug is reachable from the network in the default TCP processing path and the crash occurs before any authentication or privileged local action is required. Even where production builds do not use ASan, the logic error remains present, and malformed traffic still drives the implementation into an invalid parsing state with undefined behavior.
The affected scope is CIPster master as reproduced by me on commit 1802525be27d33e19a9a83c163e331a1d13b1892. The issue was later fixed by the maintainer in commit e8e9dba. That fix is consistent with the root cause I reported. In particular, the old branch that handled oversized packets previously disposed of the large body and then returned remaining + ENCAPSULATION_HEADER_LENGTH, which made upper layers believe a full message was present. The fix changed this behavior so that oversized packets are not reported as fully received after disposal; instead, the function returns kEipStatusError, accompanied by comments explaining that the full packet length must not be reported because the body was discarded and the packet must not be parsed. The same commit also normalized some earlier -1 returns to kEipStatusError in the same function and updated the interface comment in encap.h to state that kEipStatusError (-1) is returned on error. This repair directly addresses the receive/parse contract violation described in my report and confirms that the vulnerability had a discrete, code-level fix point.
This should be treated as an independent vulnerability entry, not merged with unrelated CIPster issues involving different root causes. The flaw here is stale-buffer reparsing caused by incorrect length reporting after oversized-packet disposal in the TCP encapsulation receive path. That is materially different from memory corruption bugs caused by object-type confusion, writable attribute misuse, or ByteBuf metadata corruption in other code paths. The vulnerable function, the trigger prerequisites, the runtime behavior, and the fix are all distinct. The issue can be independently described, independently reproduced, and independently remediated, which supports separate vulnerability tracking and a separate CVE identifier.
To summarize my findings in concise form: I discovered that CIPster mishandled oversized TCP encapsulation packets in Encapsulation::ReceiveTcpMsg(). When the declared payload length exceeded the available receive-buffer capacity, the implementation discarded the oversized body but still reported the message as fully received. HandleDataOnTcpSocket() then trusted the returned length and built a BufReader over the global buffer s_buf as if the complete packet were resident in memory. Because only the encapsulation header of the new packet had actually been written, stale bytes from a prior request in s_buf[24..] were reparsed as the current CPF payload. This stale-buffer CPF reparse extended the logical parse limit beyond the actual global object and led to a global out-of-bounds read in BufReader::get16(), causing a remotely triggerable process crash. I reported the vulnerability publicly in issue #46, demonstrated it with a reproducible ASan trace, and the maintainer fixed it in commit e8e9dba by ensuring that discarded oversized packets are returned as errors rather than as complete messages. |
|---|