Libmodbus Message Smuggling Vulnerability

Itai Shmueli
and
,
Itai Shmueli
Mar 2026
image of an infrastructure project

Libraries are very important. I’m not even talking about the kind we usually ignore! I’m talking about code libraries. And I’m not saying this lightly. Having a common implementation is so important, in fact, that in some cases, maintaining your own implementation could be considered negligent! Just imagine receiving a new pull request for review, and inside it seeing its own implementation for an HTTP parser - Heresy!

Sadly, no good deed can go unpunished, and in the case of code libraries, the downside of code sharing is bug sharing. Vulnerabilities, if we’re being more exact.

Today, we’ll be discussing a message smuggling (a.k.a interpretation conflict, a.k.a protocol desynchronization, a.k.a parser differential) vulnerability in one of the most popular  MODBUS implementations - libmodbus. Since libmodbus is integrated into thousands of products and industrial devices worldwide, this vulnerability has the potential to affect dozens of deployments across the global OT and energy ecosystem. 

The found vulnerability allows an attacker to bypass firewalls, network safeguards, and cybersecurity controls by smuggling MODBUS commands, read or write, alongside the permissible message, to any device that utilizes libmodbus. The read or write commands won’t be blocked or detected by traditional network security tools.

Despite our rigorous attempts during the disclosure process to convey to the maintainer the inner workings of the vulnerability and its impact, we received no indication that it would be patched, and it is still exploitable, as of the time of writing this. 

Since this flaw remains unpatched and can bypass standard DPI and firewall rules, operators should ensure they have continuous network-level monitoring in place to detect abnormal MODBUS behavior. Context-aware monitoring platforms like SaiFlow can identify smuggling attempts even when traditional firewalls and security tools cannot.

MODBUS/TCP Message Anatomy

MODBUS/TCP is one of the most widely adopted industrial communication protocols, enabling supervisory systems to control and monitor field devices across IoT/OT networks. Despite its simplicity and efficiency, its trust-based design leaves a lot of responsibility to its users.

In its most basic form, MODBUS/TCP is designed to facilitate the reading and writing of 16-bit and single-bit values (registers and coils, respectively) on a remote server by the client.

Let’s have a look at the anatomy of a MODBUS/TCP message:

  • Transaction ID - allows the client to distinguish which request the server was responding to.
  • Protocol ID - Currently, MODBUS defines only the value 0, which signifies MODBUS itself.
  • Length - The length of the message (in bytes) from the next field onwards (including the Unit ID field).
  • Unit ID - MODBUS supports bridging between MODBUS/TCP and MODBUS/RTU (serial) using a gateway device. The Unit ID tells the gateway to which serial device the message should be routed.
  • Function Code - Instructs what action to perform - write single coil, read multiple registers, write multiple registers, etc.
  • Data - The structure of the Data is function-specific. Common fields are addresses, values to be written, and sub-function codes.

There are a lot more details and nuances to MODBUS we could get into, but all you really need to know about it to understand this vulnerability is that the length of the Modbus message is derived from the “Length” field:

Libmodbus parsing

The Modbus protocol is everywhere in industrial, energy, and OT systems, but how that protocol gets implemented matters just as much. That’s where libmodbus comes in: a widely-adopted open-source C library used to communicate via Modbus/TCP or RTU on Linux, Windows, macOS, and embedded devices. Because libmodbus is embedded in thousands of products and applications, a bug in the library isn’t just a bug in one system, it can ripple across the entire ecosystem.

Let’s examine the parsing process of an incoming Modbus request in libmodbus:

1int _modbus_receive_msg (modbus_t * ctx, uint8_t * msg, msg_type_t msg_type) {
2  int rc;
3// ...
4  /* We need to analyse the message step by step.  At the first step, we want
5   * to reach the function code because all packets contain this
6   * information. */
7  step = _STEP_FUNCTION;
8  length_to_read = ctx ->backend ->header_length + 1; // for MODBUS/TCP header_lenght is 7, the 1 is for the function code
9// ...
10  while (length_to_read != 0) {
11// ...
12    rc = ctx ->backend ->recv (ctx, msg + msg_length, length_to_read); // calls recv syscall behind the scenes
13// ...
14    /* Sums bytes received */
15    msg_length += rc;
16    /* Computes remaining bytes */
17    length_to_read -= rc;
18
19    if (length_to_read == 0) {
20      switch (step) {
21      case _STEP_FUNCTION: // Parse fields after the function code.
22        /* Function code position */
23        length_to_read = compute_meta_length_after_function (
24          msg[ctx ->backend ->header_length], msg_type);
25        if (length_to_read != 0) {
26          step = _STEP_META;
27          break;
28        } /* else switches straight to the next step */
29      case _STEP_META: // Secondary parsing stage for things like variable length data.
30        length_to_read = compute_data_length_after_meta (ctx, msg, msg_type);
31        if ((msg_length + length_to_read) > ctx ->backend ->max_adu_length) {
32          errno = EMBBADDATA;
33          _error_print(ctx, "too many data");
34          return -1;
35        }
36        step = _STEP_DATA;
37        break;
38      default:
39        break;
40      }
41    }
42// ...
43  }
44// ...
45  return ctx ->backend ->check_integrity(ctx, msg, msg_length);
46}

And its two supporting functions:

1/* Computes the length to read after the function received */
2static uint8_t compute_meta_length_after_function (int
3  function, msg_type_t msg_type) {
4  int length;
5
6  if (msg_type == MSG_INDICATION) {
7    if (function <= MODBUS_FC_WRITE_SINGLE_REGISTER) {
8      length = 4;
9    } else if (function == MODBUS_FC_WRITE_MULTIPLE_COILS ||
10      function == MODBUS_FC_WRITE_MULTIPLE_REGISTERS) {
11      length = 5;
12    } else if (function == MODBUS_FC_MASK_WRITE_REGISTER) {
13      length = 6;
14    } else if (function == MODBUS_FC_WRITE_AND_READ_REGISTERS) {
15      length = 9;
16    } else {
17      /* MODBUS_FC_READ_EXCEPTION_STATUS, MODBUS_FC_REPORT_SLAVE_ID */
18      length = 0;
19    }
20  } else {
21    /* MSG_CONFIRMATION */
22// ...
23  }
24
25  return length;
26}

And:

1/* Computes the length to read after the meta information (address, count, etc) */
2static int
3compute_data_length_after_meta (modbus_t * ctx, uint8_t * msg, msg_type_t msg_type) {
4  int
5  function = msg [ctx ->backend ->header_length];
6  int length;
7
8  if (msg_type == MSG_INDICATION) {
9    switch (function) {
10    case MODBUS_FC_WRITE_MULTIPLE_COILS:
11    case MODBUS_FC_WRITE_MULTIPLE_REGISTERS:
12      length = msg[ctx ->backend ->header_length + 5];
13      break;
14    case MODBUS_FC_WRITE_AND_READ_REGISTERS:
15      length = msg[ctx ->backend ->header_length + 9];
16      break;
17    default:
18      length = 0;
19    }
20  } else {
21    /* MSG_CONFIRMATION */
22// ...
23  }
24
25  length += ctx ->backend ->checksum_length; // 0 for MODBUS/TCP
26
27  return length;
28}

Since even just _modbus_receive_msg contains a lot of code, I did my best to trim it while keeping the relevant parts. You can view the code in its full context here.

As we can see, libmodbus first consumes 8 bytes (MBAP header + function code). It then consumes the rest of the message in up to two more stages. How many bytes to consume in each stage is determined by compute_meta_length_after_function and compute_data_length_after_meta.

The important thing to note is that libmodbus tries to determine the packet size based on the expected structure for the given function code alone, and altogether ignores the length field (offset +4)  in the MBAP header. Nowhere in the code does it refer to the length field to determine message boundaries. This omission can cause parsing mismatches and subsequently not flush enough bytes, resulting in leftover bytes being parsed as part of the next request!

The Vulnerability

Parser differentials can manifest themselves as security vulnerabilities in several ways, one of which is request smuggling.

A classic example of it is HTTP Request Smuggling, where the server sits behind a gateway, and a conflicting interpretation of an incoming request by either one of them results in a message that should have been blocked getting processed, or it reaching somewhere it shouldn’t have.

For example, CVE-2025-55315 is an HTTP request smuggling vulnerability caused by a misinterpretation of HTTP’s chunked transfer encoding in ASP.NET Core’s Kestrel server. CVE-2020-8201 is also an HTTP request smuggling vulnerability in Node.js, caused by a misinterpretation of carriage returns.

In the case of libmodbus, the problem arises when the length specified in the MBAP header is oversized to include a payload of a secondary MODBUS message. A MODBUS-aware firewall, for example, would treat it as a single message, but a libmodbus server would interpret it as separate messages. An attacker can exploit this vulnerability to bypass the firewall by sending messages that violate its security policy, such as restricted function codes or, in the case of a MODBUS gateway implemented using libmodbus, restricted Unit IDs.

If we take a look at the following Wireshark capture:

We can see that Wireshark’s MODBUS dissector interprets one request (no. 4 - Read Coils request), with excess data, and two responses are sent back from the server (no. 6 & no. 8 - Read Coils & Write Multiple Registers responses). Any firewall, gateway, or proxy that is MODBUS-aware and respects the length field would interpret it in a similar manner.

To give a more concrete example of how this can be exploited, let’s say there’s a smart meter behind a firewall. You can read the meter’s measurements by reading specific registers, and you can reset or modify them by writing to specific registers. To avoid measurements tampering, the firewall was configured with a policy that blocks messages with function codes that aren’t exclusively for reading registers. All those protections would be for nought if the MODBUS server received a read message and interpreted it as both a read and a write one (our message smuggling vulnerability).

For reference regarding firewalls with deep packet inspection that support MODBUS, here’s an article by Palo Alto Networks, demonstrating setting up MODBUS policies - Creating a lab to test ICS/SCADA protocols, and this Fortinet documentation describing MODBUS support.

Conclusion

Message-smuggling vulnerabilities like this one demonstrate a difficult truth about OT and energy networks: it doesn’t matter how strong the perimeter is if the protocol parser inside the device sees the world differently than the firewall protecting it. When a single overlooked field, like the MBAP length, can desynchronize devices, attackers gain a powerful primitive to slip malicious operations past otherwise well-configured defenses.

Because libmodbus is embedded in thousands of products across the energy, industrial, and IoT ecosystems, this parsing flaw has consequences far beyond any single vendor. It can enable attackers to bypass firewalls, circumvent policy enforcement, trigger unauthorized function codes, or reach devices behind segmentation boundaries - all while appearing “valid” to traditional inspection tools.

This is why operators, integrators, and security teams cannot rely solely on signature-based DPI or static firewall rules. They must continuously monitor their networks for behavioral anomalies, protocol inconsistencies, and unexpected message flows.

And this is exactly what SaiFlow focuses on. Our contextual real-time monitoring solution correlates energy signals, network telemetry, and OT protocol behavior to detect anomalies that traditional tools cannot see. By understanding the operational context around every MODBUS, OCPP, IEC 61850, and OpenADR message, SaiFlow helps operators identify attacks early, before they escalate into safety, reliability, or financial-impact events.

Shared libraries bring incredible power and shared risk. As vulnerabilities like this show, the only sustainable defense is continuous visibility, contextual monitoring, and deep protocol understanding across the entire energy network.

Table of Contents