CVE-2026-48688: FastNetMon BGP MP_REACH_NLRI IPv6 Decoder Has an Acknowledged TODO | Lorikeet Security Skip to main content
Back to Blog

CVE-2026-48688: FastNetMon BGP MP_REACH_NLRI IPv6 Decoder Has an Acknowledged TODO

Lorikeet Security Team May 23, 2026 8 min read
CVSS 7.5 -- High

"TODO: we should add sanity checks" — the comment is right there in the code

CVE
CVE-2026-48688
CVSS
7.5 (High)
CWE
CWE-125 (Out-of-bounds Read)
Affected
FastNetMon Community Edition <= 1.2.9
Component
src/bgp_protocol.cpp, function decode_mp_reach_ipv6(), lines 155-209
Attack Vector
Remote (via BGP peer through GoBGP)
Discovered by
Lorikeet Security

This bug is unusual because the developer who wrote it left a comment acknowledging that the bounds checks are missing. Line 156 of src/bgp_protocol.cpp contains the text:

// TODO: we should add sanity checks to avoid reads after attribute's memory block

The TODO was never resolved. The function below it — decode_mp_reach_ipv6(), the MP_REACH_NLRI parser for the IPv6 address family — contains at least three places where attacker-controlled length fields drive memcpy sizes or pointer offsets without bounds validation against the attribute boundary.

Disclosure status: Lorikeet Security notified FastNetMon LTD on April 25, 2026. CVE-2026-48688 was assigned by MITRE. No vendor response or fix as of May 23, 2026.


The vulnerable code

// src/bgp_protocol.cpp, decode_mp_reach_ipv6(), starting around line 155
// TODO: we should add sanity checks to avoid reads after attribute's memory block

bgp_mp_reach_short_header_t* bgp_mp_ext_header =
    (bgp_mp_reach_short_header_t*)ptr;
// ^^^ Cast without verifying sizeof(bgp_mp_reach_short_header_t) bytes are available

// ... later, around line 181:
memcpy(&next_hop_ipv6.subnet_address, ptr, bgp_mp_ext_header->length_of_next_hop);
// ^^^ length_of_next_hop is attacker-controlled. No bound on memcpy size.

// ... around line 189:
uint8_t* prefix_length = ptr + sizeof(header) + length_of_next_hop + sizeof(uint8_t);
// ^^^ Pointer arithmetic across multiple attacker-controlled offsets,
//     dereferenced without checking the resulting address.

// ... around line 202:
size_t number_of_bytes_required_for_prefix =
    how_much_bytes_we_need_for_storing_certain_subnet_mask(*prefix_length);
memcpy(&prefix, /* some computed pointer */, number_of_bytes_required_for_prefix);
// ^^^ memcpy length derived from prefix_length, which itself was read OOB.

Three distinct issues live in this function:

  1. Structure cast without size verification. The function casts a raw byte pointer to bgp_mp_reach_short_header_t* and dereferences fields of that struct, without first checking that there are at least sizeof(bgp_mp_reach_short_header_t) bytes available in the attribute. If the attribute is shorter than that, the cast reads past the attribute boundary.
  2. Unbounded next-hop memcpy. The length_of_next_hop field comes from the wire. Valid values for IPv6 next-hop are 16 (one IPv6 address) or 32 (two addresses, link-local + global). An attacker can send 255. The memcpy reads 255 bytes from the attribute buffer and copies them into a fixed-size next_hop_ipv6.subnet_address field (16 bytes). This is both an over-read (reading past the attribute) and a stack-or-heap over-write (writing past the destination).
  3. Compound pointer arithmetic with no boundary check. The prefix-length pointer is computed as base + sizeof(header) + length_of_next_hop + sizeof(uint8_t). Because length_of_next_hop can be anything in [0, 255], the resulting pointer can land anywhere within a 256-byte window past the attribute boundary. Dereferencing that pointer reads whatever happens to be there into prefix_length, which then drives the next memcpy size.

Exploit primitive

An attacker sends a crafted MP_REACH_NLRI attribute carrying length_of_next_hop = 255. The first memcpy reads 255 bytes from the attribute (over-read into adjacent buffer memory). The next-hop fields are populated with those 255 bytes, but only the first 16 are addressable as subnet_address; the remaining 239 bytes overflow into whatever follows the destination struct. If next_hop_ipv6 is on the stack of decode_mp_reach_ipv6(), the overflow corrupts adjacent stack values — potentially including the saved return address.

The over-read is the most controllable primitive. Heap memory immediately following the BGP attribute buffer contains route table entries, peer state, BGP attribute caches, and route policy state. An attacker who can observe the resulting route-installation behavior (e.g., by checking which routes appear in their own routing table or by observing announcements made by GoBGP back to upstream peers) can use the over-read as a slow memory disclosure primitive.

The compound pointer arithmetic issue produces an arbitrary-read-from-fixed-offset primitive: by choosing length_of_next_hop, the attacker selects which byte of memory past the attribute gets used as prefix_length. That byte then becomes the memcpy length for the next operation, producing an additional read of attacker-chosen size.


Why the TODO was a smell, not a fix

The TODO comment is interesting because it suggests the developer noticed the danger but did not address it. There are several common reasons this happens in production codebases:

In all of these cases, the TODO is a defect-marker that should be a non-blocking issue in the project's tracker, not a comment in production code. A TODO in a security-critical parsing function for an unauthenticated network protocol is a vulnerability waiting to be assigned a CVE. The lesson, if you maintain any parser code: grep your codebase for "TODO" and "FIXME" near memcpy, pointer arithmetic, and cast operations. Each one is a future CVE.


How a fix should look

bool decode_mp_reach_ipv6(const uint8_t* attr_start, size_t attr_len, ...) {
    const uint8_t* attr_end = attr_start + attr_len;

    // 1. Verify we have at least the short header.
    if (attr_len < sizeof(bgp_mp_reach_short_header_t)) {
        return false;
    }
    const bgp_mp_reach_short_header_t* hdr =
        reinterpret_cast<const bgp_mp_reach_short_header_t*>(attr_start);

    // 2. Validate length_of_next_hop against allowed IPv6 values.
    if (hdr->length_of_next_hop != 16 && hdr->length_of_next_hop != 32) {
        return false;
    }
    const uint8_t* next_hop_start = attr_start + sizeof(*hdr);
    if (next_hop_start + hdr->length_of_next_hop > attr_end) {
        return false;
    }
    // Safe to copy:
    memcpy(&next_hop_ipv6.subnet_address, next_hop_start, 16);

    // 3. Bounds-check each pointer advance.
    const uint8_t* nlri_start = next_hop_start + hdr->length_of_next_hop + 1; // +1 for reserved
    if (nlri_start >= attr_end) {
        return false;
    }
    uint8_t prefix_length = *nlri_start;
    if (prefix_length > 128) {                 // IPv6 prefix max
        return false;
    }
    // ... and so on.
}

Three checks add up to a parser that is structurally safe. Every bound check fits on one line; they all need to be there.


Compensating controls


The recurring problem: BGP parsers are uniformly hostile

Three BGP-related CVEs in this disclosure series (CVE-2026-48685, CVE-2026-48686, and this one) plus a fourth in CVE-2026-48691 reflect a general truth about BGP-attribute parsing code: every attribute family is its own parser, every parser implements its own bounds-checking logic, and every one of them has historically had bugs. RFC 4760's multiprotocol extensions in particular — MP_REACH_NLRI and MP_UNREACH_NLRI — have produced exploitable bugs in every major BGP implementation that has shipped, including OpenBGPD, BIRD, FRRouting, Quagga, and now FastNetMon's auxiliary parser.

The structural fix at the project level is to consolidate every memcpy-from-wire and every wire-driven pointer advance into a small set of bounded-buffer helpers, then convert each parser to use those helpers. That's a refactor, not a patch, but it converts "every parser has the same bug class waiting to happen" into "every parser literally cannot have this bug class." For projects that consume BGP attributes from any source, the refactor is worth doing once.


Disclosure timeline

DateEvent
2026-04-25Vulnerability identified during Lorikeet Security source code audit of FastNetMon Community Edition 1.2.9
2026-04-25CVE ID requested from MITRE
2026-04-25Vendor (Pavel Odintsov / FastNetMon LTD) notified at the contact published in SECURITY.md
2026-05-22CVE-2026-48688 assigned by MITRE
TBDVendor response
TBDFix release
2026-05-23Lorikeet 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.

Download PDF

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.

-- views
Link copied!
Lorikeet Security

Lorikeet Security Team

Penetration Testing & Cybersecurity Consulting

Lorikeet Security helps modern engineering teams ship safer software. Our work spans web applications, APIs, cloud infrastructure, and AI-generated codebases — and everything we publish here comes from patterns we see in real client engagements.

Lory waving

Hi, I'm Lory! Need help finding the right service? Click to chat!