Critical Buffer Overflow in ShadowsockR: How memcpy Became a Security Nightmare
Severity: 🔴 Critical | CVE Class: CWE-122 (Heap-based Buffer Overflow) | Affected Component:
shadowsocksr-libev/src/server/server.c
Introduction
In the world of network proxy software, trust is everything — and the boundary between trusted and untrusted data must be defended at every single line of code. A recently patched vulnerability in the ShadowsockR proxy server (shadowsocksr-libev) demonstrates exactly what happens when that boundary breaks down: a critical buffer overflow that could allow a remote attacker to corrupt server memory simply by sending a specially crafted network packet.
This vulnerability is a textbook example of never trusting attacker-controlled input, a principle that has been taught in security courses for decades yet continues to appear in real-world codebases. If you write C or C++ code that processes network data, this post is required reading.
What Is a Buffer Overflow?
Before diving into the specifics, let's ground ourselves in the fundamentals.
A buffer overflow occurs when a program writes more data into a memory buffer than the buffer was allocated to hold. The excess data spills over into adjacent memory regions, overwriting whatever happened to be stored there — which could be other variables, function return addresses, or control flow data.
In C, the memcpy function is one of the most common sources of this bug. Its signature is deceptively simple:
void *memcpy(void *dest, const void *src, size_t n);
The third argument, n, tells memcpy exactly how many bytes to copy. If n comes from an attacker — say, from a field in a network packet — and you don't validate it against the size of your destination buffer, you've handed an attacker a powerful memory corruption primitive.
The Vulnerability Explained
What Went Wrong
Inside shadowsocksr-libev/src/server/server.c, the server processes incoming network packets that describe proxy connection targets. Part of this processing involves reading an address (hostname) from the packet and copying it into a local buffer called host.
The critical flaw: the length of the address (name_len) was read directly from the attacker-controlled packet and passed to memcpy without any validation.
Here's a simplified representation of the vulnerable pattern:
// ❌ VULNERABLE CODE (simplified for illustration)
char host[256]; // Fixed-size buffer on the stack
// name_len comes directly from the network packet — attacker controlled!
uint8_t name_len = packet_data[offset];
// No bounds check! If name_len > 256, we overflow the buffer.
memcpy(host, packet_data + offset + 1, name_len);
This pattern appeared at four separate locations in the file: lines 687, 796, 935, and 1055. Each represents an independent attack surface.
How Could It Be Exploited?
The attack scenario is straightforward and requires no authentication:
- Attacker connects to the ShadowsockR server (it's a proxy — that's its job).
- Attacker crafts a malicious packet where the address length field (
name_len) is set to a value larger than the destinationhostbuffer (e.g., 255 bytes when the buffer is 64 bytes). - Server calls
memcpywith the attacker-controlled length, writing past the end ofhost. - Adjacent memory is corrupted, which depending on what lives there could:
- Crash the server (Denial of Service)
- Overwrite a function return address (enabling arbitrary code execution)
- Corrupt other connection state (enabling privilege escalation or data leakage)
Why Is This Particularly Dangerous?
Several factors elevate this from "bad bug" to "critical vulnerability":
| Factor | Impact |
|---|---|
| No authentication required | Any network-reachable attacker can trigger it |
| Remote exploitability | No local access needed |
| Four affected locations | Larger attack surface, harder to miss in exploitation |
| Proxy context | Servers are often internet-facing by design |
| Potential for RCE | Stack/heap corruption can lead to code execution |
A proxy server is, by nature, designed to accept connections from potentially untrusted clients. This means the vulnerable code path is trivially reachable by any attacker who can connect to the server's port.
Real-World Attack Scenario
Imagine a corporate network using ShadowsockR to route traffic for remote workers. The proxy server is exposed on a known port. An attacker, without any credentials or prior knowledge of the network, sends a single malformed packet:
[SOCKS5 Header][Address Type: DOMAIN][Length: 0xFF][255 bytes of 'A']
The server reads 0xFF (255) as name_len and dutifully copies 255 bytes into a buffer that might only hold 64. The stack frame is corrupted. With a carefully constructed payload instead of 'A' characters, the attacker can redirect execution to shellcode — achieving Remote Code Execution on the proxy server, which likely has access to the internal network.
The Fix
What Changed
The fix introduces bounds checking before every memcpy call that uses a network-supplied length value. The pattern is consistent across all four affected locations:
// ✅ FIXED CODE (simplified for illustration)
char host[256]; // Fixed-size buffer
#define MAX_HOSTNAME_LEN 255 // Or sizeof(host) - 1
uint8_t name_len = packet_data[offset];
// Validate BEFORE copying
if (name_len == 0 || name_len > sizeof(host) - 1) {
// Reject the packet — invalid length
LOGE("Invalid hostname length: %d", name_len);
close_connection(server);
return;
}
// Now safe to copy
memcpy(host, packet_data + offset + 1, name_len);
host[name_len] = '\0'; // Null-terminate safely
Why This Fix Works
The guard clause is simple but powerful:
- It validates the untrusted value (
name_len) against the known-safe maximum (the size of the destination buffer) before any memory operation occurs. - It rejects malformed packets rather than attempting to process them, following the principle of failing safely.
- It's applied consistently at all four vulnerable locations, eliminating the entire class of vulnerability in this code path.
The Broader Principle
Notice that the fix doesn't try to "truncate" the copy to fit the buffer. It rejects the packet entirely. This is the correct approach: a legitimate SOCKS5 client will never send a hostname longer than the protocol allows. An oversized name_len is unambiguously malicious or malformed, and the right response is rejection, not accommodation.
Prevention & Best Practices
1. Never Trust Network-Supplied Lengths
Any length, offset, or size value that arrives from a network packet, user input, or any external source must be treated as hostile until validated.
// The golden rule: validate BEFORE use
if (untrusted_length > sizeof(destination_buffer)) {
handle_error();
return;
}
memcpy(destination_buffer, source, untrusted_length);
2. Use Safer Alternatives to memcpy
Where possible, prefer functions that incorporate bounds checking:
// Use memcpy_s (C11 Annex K) where available
errno_t err = memcpy_s(dest, dest_size, src, count);
if (err != 0) {
// Handle error
}
// Or OpenBSD's explicit_bzero + manual bounds check pattern
// Or use higher-level abstractions in C++: std::vector, std::string
3. Consider Memory-Safe Languages for New Code
The Rust programming language, notably, makes this entire class of bug impossible by default. Buffer accesses are bounds-checked at compile time or runtime, and the type system prevents many unsafe patterns. For new proxy or network code, Rust is worth serious consideration.
// Rust equivalent — bounds checked automatically
let name_len = packet[offset] as usize;
if name_len > host.len() {
return Err(ProtocolError::InvalidLength);
}
host[..name_len].copy_from_slice(&packet[offset+1..offset+1+name_len]);
4. Use Static Analysis Tools
Several tools can catch this class of vulnerability automatically:
| Tool | Type | Notes |
|---|---|---|
| Coverity | Static Analysis | Industry standard for C/C++ |
| AddressSanitizer (ASan) | Runtime | Catches overflows in testing |
| Valgrind | Runtime | Memory error detection |
| CodeQL | Semantic Analysis | GitHub-integrated, finds data flow issues |
| Clang Static Analyzer | Static Analysis | Free, built into LLVM |
| Fuzzing (libFuzzer/AFL++) | Dynamic | Finds crashes from malformed input |
Integrating these into your CI/CD pipeline means vulnerabilities like this are caught before they reach production.
5. Apply the Principle of Input Validation at Boundaries
Every time data crosses a trust boundary (network → application, user → kernel, etc.), validate it completely:
- ✅ Check minimum and maximum lengths
- ✅ Validate that values are within expected ranges
- ✅ Verify structural integrity before processing
- ✅ Reject and log anomalies rather than silently truncating
6. Relevant Security Standards
This vulnerability maps to several well-known security standards:
- CWE-122: Heap-based Buffer Overflow
- CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')
- CWE-805: Buffer Access with Incorrect Length Value
- OWASP: A03:2021 – Injection (memory corruption as a subclass)
- CERT C: Rule ARR38-C — Guarantee that library functions do not form invalid pointers
- SEI CERT C Coding Standard: MEM35-C — Allocate sufficient memory for an object
Lessons for Code Reviewers
This vulnerability has a characteristic pattern that reviewers should watch for:
// 🚩 Red flags to look for in code review:
// 1. Length from external source used directly in memory operations
size_t len = external_data->length; // Where does this come from?
memcpy(buf, src, len); // No validation!
// 2. Fixed-size buffers receiving variable-length network data
char hostname[64];
memcpy(hostname, packet + offset, packet_hostname_len); // 🚩
// 3. Missing null-terminator after memcpy of string data
memcpy(str_buf, src, len);
// str_buf[len] = '\0'; // Missing! Could cause further issues
Make bounds validation a mandatory checklist item for any code review involving:
- Network packet parsing
- File format parsing
- Inter-process communication (IPC)
- Any memcpy, strcpy, sprintf, or similar functions
Conclusion
The buffer overflow vulnerability in ShadowsockR's server.c is a stark reminder that in C, the compiler will not save you from yourself. A single missing bounds check, repeated across four locations, transformed a routine memory copy into a critical remote code execution primitive accessible to any attacker who can connect to the server.
The fix is elegant in its simplicity: validate the length before you use it, and reject anything that doesn't fit. This is not new wisdom — it's been in every C security guide since the 1990s. Yet it continues to appear in production code, which is why automated scanning, rigorous code review, and security-focused testing are not optional extras but essential parts of the software development lifecycle.
Key takeaways:
- 🔑 Never use attacker-controlled values as sizes in memory operations without validation
- 🔑 Validate at trust boundaries — every single one
- 🔑 Prefer rejection over accommodation for malformed input
- 🔑 Use static analysis and fuzzing to catch what human reviewers miss
- 🔑 Consider memory-safe languages for security-critical network code
Security is not a feature you add at the end — it's a discipline you apply at every line. Stay safe, validate your inputs, and keep shipping secure code.
This vulnerability was identified and patched by OrbisAI Security's automated security scanning pipeline. Automated security tooling combined with human review represents the current best practice for catching vulnerabilities before they reach production.
References:
- CERT C Coding Standard - MEM35-C
- CWE-122: Heap-based Buffer Overflow
- OWASP Buffer Overflow
- Google Project Zero: Exploiting Buffer Overflows
- AddressSanitizer Documentation