Severity: Critical | CWE: CWE-120 (Buffer Copy without Checking Size of Input) | File:
upnp/src/ssdp/ssdp_ctrlpt.c
Introduction
Buffer overflows are among the oldest and most dangerous vulnerability classes in systems programming — and they keep showing up in production code. This post dives into a critical heap buffer overflow discovered in the SSDP (Simple Service Discovery Protocol) control point implementation of a UPnP library, explains exactly how it could be exploited, and walks through the fix that closes the door on this attack vector.
If you write C or C++ code that processes network data, this one is for you. Even if you don't, understanding why unbounded string operations are dangerous is essential knowledge for any developer who cares about the security of the systems they build.
What Is SSDP and Why Does It Matter?
SSDP (Simple Service Discovery Protocol) is the discovery mechanism at the heart of UPnP (Universal Plug and Play). It's how your smart TV finds your router, how a media server announces itself to clients, and how IoT devices broadcast their capabilities on a local network.
The control point side of SSDP is responsible for sending M-SEARCH requests — essentially asking "who's out there?" — and processing the responses. Because SSDP operates over UDP and accepts responses from any device on the network, it is a natural target for network-based attacks. Malicious or malformed responses are trivial to craft and inject.
The Vulnerability Explained
What Went Wrong
Inside CreateClientRequestPacket(), the code was responsible for building an HTTP-style M-SEARCH request buffer (RqstBuf). The original implementation did this in several stages using a combination of strcpy, strcat, and snprintf into an intermediate TempBuf:
// VULNERABLE CODE (before the fix)
char TempBuf[COMMAND_LEN];
const char *command = "M-SEARCH * HTTP/1.1\r\n";
const char *man = "MAN: \"ssdp:discover\"\r\n";
memset(TempBuf, 0, sizeof(TempBuf));
if (RqstBufSize <= strlen(command))
return UPNP_E_INTERNAL_ERROR;
strcpy(RqstBuf, command); // ⚠️ No overflow check after this point
// ... snprintf into TempBuf for the HOST header ...
if (RqstBufSize <= strlen(RqstBuf) + strlen(TempBuf))
return UPNP_E_BUFFER_TOO_SMALL;
strcat(RqstBuf, TempBuf); // ⚠️ Race between check and use
if (RqstBufSize <= strlen(RqstBuf) + strlen(man))
return UPNP_E_BUFFER_TOO_SMALL;
strcat(RqstBuf, man); // ⚠️ Another unbounded append
// ... and so on for MX header, ST header, etc.
There are several compounding problems here:
-
strcpywithout bounds checking — The initialstrcpy(RqstBuf, command)checks thatRqstBufSize > strlen(command), but this check only validates the first write. All subsequentstrcatcalls append to a buffer that is being filled incrementally, with each check only loosely validating the current state. -
TOCTOU-style logic errors — The pattern
if (RqstBufSize <= strlen(RqstBuf) + strlen(TempBuf)) return error; strcat(RqstBuf, TempBuf);is fragile. The length check and the write are not atomic, and more importantly, the check uses<=instead of<, meaning a perfectly full buffer passes the check and then overflows by one byte on thestrcat. -
Network-controlled input flows into the buffer — Fields like service type (
ST) headers and location URLs come directly from network-received SSDP responses. An attacker on the same network segment can craft a response with an oversizedSTfield. When this value is concatenated intoRqstBufwithout strict length enforcement, the buffer overflows onto the heap.
How Could This Be Exploited?
Here's a realistic attack scenario:
- An attacker connects to the same local network as a device running the vulnerable UPnP control point (common in home networks, hotel Wi-Fi, corporate LANs, or any shared network).
- The control point broadcasts an SSDP
M-SEARCHrequest. - The attacker's machine responds with a crafted SSDP response containing an oversized
ST(Service Type) field — for example, a string of several kilobytes instead of the expected short identifier likeupnp:rootdevice. - The vulnerable code copies this attacker-controlled string into
RqstBufviastrcat, writing far beyond the allocated heap buffer. - The overflow corrupts adjacent heap metadata or data, potentially enabling:
- Arbitrary code execution via heap metadata manipulation
- Denial of service via heap corruption and crash
- Information disclosure if heap layout can be influenced to leak adjacent memory
Because SSDP uses UDP and requires no authentication, this attack requires no credentials, no prior access, and no user interaction — just network adjacency.
CWE-120: The Classic "Dangerous Function" Problem
This vulnerability is classified under CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow'). The C standard library functions strcpy and strcat are considered dangerous precisely because they perform no bounds checking. The destination buffer's size is never consulted — they simply write until they hit a null terminator in the source string.
The CERT C Coding Standard, MISRA C, and virtually every modern secure coding guideline explicitly prohibit the use of strcpy and strcat in code that handles externally-sourced data.
The Fix
What Changed
The fix replaces the fragmented, multi-step string construction approach with a single, offset-tracked snprintf pattern. Here's the corrected code:
// FIXED CODE (after the patch)
size_t offset = 0;
switch (AddressFamily) {
case AF_INET:
rc = snprintf(RqstBuf,
RqstBufSize,
"M-SEARCH * HTTP/1.1\r\n"
"HOST: %s:%d\r\n"
"MAN: \"ssdp:discover\"\r\n",
SSDP_IP,
SSDP_PORT);
break;
case AF_INET6:
rc = snprintf(RqstBuf,
RqstBufSize,
"M-SEARCH * HTTP/1.1\r\n"
"HOST: [%s]:%d\r\n"
"MAN: \"ssdp:discover\"\r\n",
SSDP_IPV6_LINKLOCAL,
SSDP_PORT);
break;
default:
return UPNP_E_INVALID_ARGUMENT;
}
if (rc < 0 || (size_t)rc >= RqstBufSize)
return UPNP_E_INTERNAL_ERROR;
offset = (size_t)rc;
if (Mx > 0) {
rc = snprintf(RqstBuf + offset,
RqstBufSize - offset,
"MX: %d\r\n",
Mx);
if (rc < 0 || (size_t)rc >= RqstBufSize - offset)
return UPNP_E_BUFFER_TOO_SMALL;
offset += (size_t)rc;
}
Why This Fix Works
Let's break down the security improvements:
1. snprintf Enforces Hard Limits
snprintf(dest, n, ...) will never write more than n bytes (including the null terminator) to the destination buffer. There is no scenario in which it overflows — if the formatted output would exceed the limit, it is truncated and the function returns the number of bytes that would have been written, allowing the caller to detect truncation.
2. Offset Tracking Eliminates Cumulative Errors
Instead of using strlen(RqstBuf) to find the current end of the buffer (which is O(n) and error-prone), the fix maintains an explicit offset variable. Each snprintf call writes to RqstBuf + offset with a remaining capacity of RqstBufSize - offset. This is both more efficient and more correct — there's no possibility of miscounting.
3. Truncation Is Detected and Rejected
After each snprintf, the return value rc is checked:
- rc < 0 → encoding error, return failure
- (size_t)rc >= RqstBufSize - offset → output was truncated (or would have been), return failure
This means the code fails safely rather than silently producing a malformed or truncated buffer that could cause downstream issues.
4. The Intermediate Buffer Is Eliminated
The original code used TempBuf[COMMAND_LEN] as a staging area, introducing an extra potential overflow point. The fix writes directly into RqstBuf with full size awareness, removing an entire class of intermediate buffer issues.
5. Consolidation Reduces Attack Surface
By consolidating the static header lines (M-SEARCH, HOST, MAN) into a single snprintf call, the fix reduces the number of operations that touch the buffer — fewer operations means fewer opportunities for mistakes.
Before vs. After: A Side-by-Side Comparison
| Aspect | Before (Vulnerable) | After (Fixed) |
|---|---|---|
| String functions used | strcpy, strcat, snprintf |
snprintf only |
| Bounds enforcement | Manual, error-prone checks | Enforced by snprintf |
| Buffer position tracking | strlen(RqstBuf) (O(n), fragile) |
Explicit offset variable |
| Truncation detection | Partial (only for TempBuf) |
Full (every write checked) |
| Intermediate buffers | TempBuf[COMMAND_LEN] |
None |
| Network input handling | Unsafe concatenation | Bounded write with failure |
Key Takeaways
-
strcpyandstrcatare dangerous — they perform no bounds checking and should never be used with data that originates from the network or user input. -
The "check then use" pattern is fragile — checking
strlenbeforestrcatis not sufficient. Usesnprintfwith explicit size limits instead. -
Network protocols are attack surfaces — SSDP operates without authentication on local networks. Any device on your Wi-Fi can send crafted SSDP responses. Buffer overflows in SSDP parsers are reachable with zero credentials.
-
The fix is simple and elegant — replacing a tangle of
strcpy/strcatcalls with offset-trackedsnprintfcalls eliminates the vulnerability class entirely, not just the specific instance. -
Defense in depth matters — compiler hardening, runtime sanitizers, and static analysis complement correct code. Use all of them.
Buffer overflows in C have been known since the 1970s and have been responsible for some of the most devastating exploits in computing history — from the Morris Worm to countless remote code execution vulnerabilities in network daemons. The fact that they still appear in modern codebases is a reminder that secure coding requires constant vigilance, good tooling, and a healthy skepticism toward any code that touches a network socket.
Write safe code. Validate your inputs. And when in doubt, use snprintf.
This vulnerability was identified and fixed by OrbisAI Security using automated security scanning and AI-assisted code review.