In late 2025, I discovered a vulnerability in some TP-Link Smart Switches that could be exploited to obtain unauthenticated remote code execution as root (CVE-2026-1668).

This was reported to security@tp-link.com, and in mid-March 2026, a fixed version of the firmware was made available. TP-Link has also published a security advisory, recommending users to update their firmware on affected devices. If you have an affected device, I recommend you update the firmware immediately.

It is now 90+30 days after the initial report and a fix is available. It’s time to discuss the vulnerability in more detail.

The vulnerability

The vulnerability exists in the httpJsonPost function of the httpd binary, which reads and returns the body of a POST request as a C string. Here is the relevant excerpt of decompiler output from Ghidra that shows the logic:

pcVar1 = (char *)httpMimeHdrGet(req,0,"Content-Length");
contentLength = strtoul(pcVar1,(char **)0x0,0);
if (contentLength != 0) {
  p = (mem_pool_t *)httpReqMemPartIdGet(req);
  buf = (char *)memPoolAlloc(p,contentLength + 1);
  if (buf == (char *)0x0) {
    wmError(-100,"httpJsonPost: unable to allocate mem for entity");
    return -1;
  }
  n = 0;
  i = 0;
  do {
    readBytes = httpBlockRead(req,buf + n,contentLength - n);
    i = i + 1;
    n = n + readBytes;
    if ((readBytes == -1) || (contentLength == n)) break;
    usleep(2000);
  } while (i != 0x14);
  buf[contentLength] = '\0';
  if (out != (char **)0x0) {
    *out = buf;
    return 0;
  }
}

At a high-level, the logic does the following:

  1. Reads the Content-Length header value as a string.
  2. Use strtoul to parse the string into contentLength, as an unsigned integer.
  3. Check whether contentLength is zero (i.e. the request included a correctly-formatted, non-zero integer as the Content-Length).
  4. Allocate contentLength + 1 bytes of memory to store the request + null byte to terminate the buffer into a C string.
  5. Check whether the memory allocation failed.
  6. Read up to contentLength bytes of data into the allocated buffer.

The problem with this logic is that it does not take into account of integer overflow. If contentLength is set to 4294967295, it passes the length check, but overflows to zero once incremented. Thus, memPoolAlloc is actually called with an argument of zero, and the implementation happily returns a pointer to a zero-sized block of memory. At this point, any write to the returned pointer is an out-of-bounds write.

For non-HTTPS requests, httpBlockRead is essentially a wrapper around a read syscall. On the Linux kernel version running on the device, the read syscall is happy to take a large size (i.e. 4294967295) parameter, and just do short reads (we do not need to actually send 4294967295 bytes of payload). FWIW, this did not work when I tested this under QEMU emulation - instead, read just immediately returns an EFAULT error.

Unfortunately, on HTTPS requests, httpBlockRead ends up calling the OpenSSL SSL_Read function, which treats the size parameter as a signed integer and fails the early length-check for negative sizes (because 4294967295 in interpreted as -1). Because of this, I have not found a way to trigger this vulnerability when using HTTPS. However, this is a moot point because the firmware still responds to HTTP API requests on a default installation of the latest firmware at the time.

The fix

This vulnerability was fixed by improving the length checking. Previously, it was simply a comparison for zero, allowing for overflow when evaluating contentLength + 1:

if (contentLength != 0)

The improved logic now checks that the result of contentLength + 1 will never exceed 0x20000:

if (contentLength - 1 < 0x20000)

Having looked at the assembly code, this is indeed an unsigned integer comparison, so the check should work even if contentLength - 1 underflows. The comparison will be against 0xFFFFFFFF, which is greater than 0x20000, failing the check as intended.

As far as I can tell, this improved length checking is sufficient for patching the vulnerability.

Triggering the vulnerability

The most convenient way to trigger the vulnerable httpJsonPost function is via the /data/login.json HTTP endpoint because it does not require any authentication, and is always available.

We can send the following HTTP request to trigger the vulnerability:

POST /data/login.json HTTP/1.1
Content-Type: application/json
Content-Length: 4294967295

Any data we include in the body of the POST request can be used to overwrite whatever happens to be in memory below the buffer.

In the next post, we will look at how we can exploit this vulnerability to gain arbitrary code execution.


Disclosure timeline

  • 2025-11-22: Vulnerability reported to security@tp-link.com.
  • 2025-11-26: TP-Link acknowledged receipt of the report, pending initial triage.
  • 2025-12-23: Sent a follow-up email, asking for updates.
  • 2026-01-10: TP-Link acknowledged the vulnerability, pending verification and remediation.
  • 2026-01-15: Sent a follow-up email, confirming the vulnerability is still exploitable on the latest firmware, and asking for an update on remediation.
  • 2026-01-26: Sent another follow-up asking for updates, after no reply.
  • 2026-01-28: TP-Link claimed the vulnerability has been addressed in their development build.
  • 2026-01-29: TP-Link sent a pre-release firmware image for verifying that the vulnerability has been sufficiently addressed.
  • 2026-01-29: Sent confirmation that the vulnerability appeared to be sufficiently addressed.
  • 2026-01-30: Vulnerability assigned to CVE-2026-1668.
  • 2026-02-12: TP-Link asked for an extension to the disclosure deadline.
  • 2026-02-12: Granted extension until 2026-03-16.
  • 2026-02-20: 90 days elapsed since initial vulnerability report.
  • 2026-03-14: TP-Link made fixed firmwares public, and published security advisory.
  • 2026-03-22: 90+30 days elapsed since initial vulnerability report.