Stack Buffer Overflow via Unvalidated BGP NLRI Prefix Length — Remote Code Execution
- CVE
- CVE-2026-48686
- CVSS
- 9.8 (Critical)
- CWE
- CWE-787 (Out-of-bounds Write), CWE-120 (Buffer Copy without Checking Size)
- Affected
- FastNetMon Community Edition <= 1.2.9
- Component
- src/bgp_protocol.cpp, function
decode_bgp_subnet_encoding_ipv4_raw(), lines 93-111 - Attack Vector
- Remote (via BGP peer through GoBGP)
- Discovered by
- Lorikeet Security
This is the bug we worried about when we started reading the FastNetMon BGP code. The function decode_bgp_subnet_encoding_ipv4_raw() in src/bgp_protocol.cpp reads a prefix bit length from a BGP NLRI byte stream, derives a byte count from it, and then performs a memcpy using that byte count into a stack variable that has room for exactly four bytes. There is no bounds check between "I read a byte from the wire" and "I memcpy that many bytes onto my stack."
The math is precise. For IPv4 NLRI, the prefix bit length must be in [0, 32]. If the attacker sets it to 255, the helper how_much_bytes_we_need_for_storing_certain_subnet_mask() computes ceil(255 / 8) = 32. The destination variable is uint32_t prefix_ipv4, which is exactly 4 bytes on the stack. The memcpy(&prefix_ipv4, value + 1, 32) writes 32 bytes into a 4-byte slot, overflowing 28 bytes past the variable.
This is a remote code execution primitive. The default FastNetMon CMake build has zero security flags: no -fstack-protector, no -D_FORTIFY_SOURCE=2, no -fPIE/-pie, no RELRO. There is no stack canary to detect the overflow, no FORTIFY-instrumented memcpy to refuse the oversize copy, and the binary itself is at a static base. 28 bytes is more than enough to overwrite the saved return address with an attacker-controlled value.
The vulnerable code
// src/bgp_protocol.cpp around line 93
bool decode_bgp_subnet_encoding_ipv4_raw(const uint8_t* value, ...) {
uint8_t prefix_bit_length = value[0];
// ^^^ Attacker can set this to anything in [0, 255]. No validation.
uint32_t prefix_byte_length =
how_much_bytes_we_need_for_storing_certain_subnet_mask(prefix_bit_length);
// For prefix_bit_length = 255, returns ceil(255 / 8) = 32.
uint32_t prefix_ipv4 = 0; // 4 bytes on the stack
memcpy(&prefix_ipv4, value + 1, prefix_byte_length);
// ^^^ Writes up to 32 bytes into a 4-byte variable.
// Overflow is exactly prefix_byte_length - 4 bytes (up to 28 bytes).
// Source bytes come directly from the attacker.
convert_cidr_to_binary_netmask_local_function_copy(prefix_bit_length, ...);
// ^^^ Secondary issue: shift of (32 - cidr) for cidr > 32 is undefined behavior.
}
Two distinct problems live in this function. The headline issue is the stack overflow. The secondary issue is that convert_cidr_to_binary_netmask_local_function_copy(prefix_bit_length, ...) computes 32 - cidr and then shifts by that amount. For cidr > 32, the subtraction wraps in unsigned arithmetic (producing a huge value), and the subsequent shift is undefined behavior per the C++ standard. In practice on x86, the shift count is masked to 5 bits, but on other architectures the behavior is genuinely undefined and could produce additional memory corruption.
Exploit picture
The 28-byte overflow lands in the stack frame of decode_bgp_subnet_encoding_ipv4_raw(). On x86_64 Linux with SysV ABI, the immediate targets in that frame are:
- Saved RBP (8 bytes) — the previous frame's base pointer
- Saved return address (8 bytes) — the address to jump to when this function returns
- Caller's stack variables — anything else above this frame
28 bytes of attacker-controlled write past a 4-byte variable comfortably reaches the saved return address. Because the binary is built without -fPIE/-pie, the text section is at a static base; the attacker can hardcode addresses of gadgets in the FastNetMon binary or libc (libc is usually ASLR-randomized, but the FastNetMon text is not). The combination of "no stack canary" + "no PIE" + "attacker controls saved return address" is the textbook exploitable stack overflow.
The values written are the bytes that follow the prefix_bit_length byte in the NLRI — those are entirely attacker-controlled because they were originally the prefix bytes themselves. The attacker can write any 32-byte payload by encoding it as a BGP NLRI with prefix_bit_length = 255 followed by 32 chosen bytes.
Reachability
Like CVE-2026-48685, this bug is reached through the external BGP daemon (GoBGP or ExaBGP) that FastNetMon receives BGP data from. The attacker has to be a BGP peer of that daemon. In practice, this means:
- A malicious upstream provider who is already a configured BGP peer. The trust assumption is that your peers don't actively try to compromise you; this CVE breaks that assumption for an attacker who is willing to use their peering relationship offensively.
- A compromised upstream router at a peering point or transit provider. Any router that already has a BGP session with one of your speakers can send malicious NLRI.
- An attacker who can BGP-hijack or man-in-the-middle a BGP session. BGP runs over TCP, not over TLS. A network attacker on the path between your speaker and a legitimate peer can inject UPDATE messages.
The "Critical" CVSS rating reflects that, given a viable BGP peering position, an attacker can achieve unauthenticated remote code execution as the FastNetMon process user on the host. FastNetMon typically runs with elevated privileges for raw packet capture, so the post-exploitation impact is high.
How a fix should look
bool decode_bgp_subnet_encoding_ipv4_raw(const uint8_t* value, ...) {
uint8_t prefix_bit_length = value[0];
// Validate before doing anything else.
if (prefix_bit_length > 32) {
return false; // IPv4 prefix length cannot exceed 32 bits.
}
uint32_t prefix_byte_length =
how_much_bytes_we_need_for_storing_certain_subnet_mask(prefix_bit_length);
// After the <= 32 check, this is guaranteed to be in [0, 4].
uint32_t prefix_ipv4 = 0;
memcpy(&prefix_ipv4, value + 1, prefix_byte_length);
// Safe: prefix_byte_length is now in [0, 4] which fits in uint32_t.
convert_cidr_to_binary_netmask_local_function_copy(prefix_bit_length, ...);
// Safe: cidr is now <= 32.
}
The fix is one line. The conditional should be at the top of the function, before any memory is touched, and should be a strict > 32 check, not >= 32 (a /32 host route is a legitimate value).
The structural improvement is to never use memcpy into a fixed-size stack variable with a length that came from the wire. The destination should always have a static size, the source should be bounded by that size, and the copy length should be std::min(static_dest_size, wire_length). That pattern eliminates the entire class of "stack overflow from wire-supplied length" bugs.
The bigger problem: the build has no hardening
The reason this is a critical and not a high is the lack of standard compiler hardening in the default FastNetMon build. Modern compilers offer four cheap, well-tested mitigations that would significantly raise the cost of exploiting a stack overflow:
| Mitigation | What it does | FastNetMon default build |
|---|---|---|
-fstack-protector-strong | Inserts a stack canary before the saved return address. Detects most stack overflows at function return. | Not enabled |
-D_FORTIFY_SOURCE=2 | Compile-time and run-time checks for memcpy, strcpy, sprintf, etc. with known destination sizes. Aborts on overflow. | Not enabled |
-fPIE / -pie | Position-independent executable. Makes the text section ASLR-eligible. Defeats hardcoded gadget addresses. | Not enabled |
-Wl,-z,relro -Wl,-z,now | Makes the GOT/PLT read-only after relocation. Stops common ret2plt exploitation patterns. | Not enabled |
Adding these flags is two lines in CMakeLists.txt. They cost essentially nothing at runtime. The fact that none of them are enabled means a 28-byte stack overflow goes from "would have been caught by the stack canary and the process would have aborted" to "directly exploitable for code execution." Operators who maintain their own packaging should add these flags to their build before any of the CVEs in this series are even patched.
Compensating controls
- Restrict your BGP peer set to trusted peers. The attacker has to be a BGP peer of GoBGP/ExaBGP to reach this bug. Audit the configured peers; remove anything not strictly needed.
- Rebuild FastNetMon with hardening flags. If you maintain your own package, add
-fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIEto CFLAGS and-pie -Wl,-z,relro -Wl,-z,nowto LDFLAGS in CMakeLists.txt. A stack canary alone would detect this overflow and terminate the process — converting RCE into a much-less-bad DoS. - Run FastNetMon under a privilege-separated user. The bug gives RCE as the FastNetMon process user. If that user is root (which is the default for raw-capture mode), the attacker becomes root. Move the daemon to a low-privilege user with only
CAP_NET_RAWset on the binary, and post-exploitation is meaningfully more constrained. - Watch BGP session logs for malformed UPDATE messages or unexpected NLRI lengths from any peer. GoBGP's debug logging is verbose enough that abnormal NLRI patterns are visible.
- Disable BGP entirely if you don't use it. Many FastNetMon deployments do flow-based detection only and do not need the BGP integration. If you don't consume route data, the BGP plugin doesn't need to be enabled.
The lesson: validate the bit length, every time
Every protocol that encodes IP prefixes as "prefix length followed by N bytes of prefix data" has produced this bug at least once. RFC 4271 specifies the bit-length encoding for BGP NLRI. RFC 5101 specifies it for IPFIX. RFC 4760 specifies it for MP-BGP NLRI families. In every case, the bit length must be validated against the address family's maximum (32 for IPv4, 128 for IPv6) before any byte-count derivation. This is the single most well-known footgun in IP-prefix wire-format parsing, and it produces an exploitable bug every time it gets missed.
If you maintain a BGP parser, an IPFIX collector, a SDN-controller route processor, or any code that converts a bit length to a byte count for a stack copy, this is the line you want highlighted in red in your code review checklist. The cost of the bounds check is zero. The cost of missing it, in this case, is unauthenticated remote code execution as root.
Disclosure timeline
| Date | Event |
|---|---|
| 2026-04-25 | Vulnerability identified during Lorikeet Security source code audit of FastNetMon Community Edition 1.2.9 |
| 2026-04-25 | CVE ID requested from MITRE |
| 2026-04-25 | Vendor (Pavel Odintsov / FastNetMon LTD) notified at the contact published in SECURITY.md |
| 2026-05-22 | CVE-2026-48686 assigned by MITRE |
| TBD | Vendor response |
| TBD | Fix release |
| 2026-05-23 | Lorikeet Security publishes responsible disclosure report |
Full Responsible Disclosure Report (PDF)
Complete writeup of all 16 FastNetMon Community Edition vulnerabilities Lorikeet Security identified, including vulnerable-code excerpts, impact analysis, and remediation guidance for each CVE.
Auditing the network infrastructure your business depends on
Lorikeet Security finds the parser bugs, protocol confusion, and unauthenticated-control-plane issues that move your DDoS detector, your BGP speaker, and your edge devices from "running" to "exploitable." Source code review, fuzz harness development, and adversarial protocol testing.