High Impact

CVE-2026-34473 Unauthenticated Denial of Service in ZTE Routers affecting 140K+ devices worldwide (17+ models)

Research thumbnail showing the scale estimate used during the original disclosure period
Exposure estimate from the original 2024 Shodan workset: 140K+ observed devices across 17+ models.

A pre-auth oversized application/x-www-form-urlencoded POST can drive the router web interface into denial of service. The root cause sits in the request-body handling path: attacker-controlled POST data reaches the CGILua parser before authentication, and the parser eagerly reads and processes request bodies that are still within the configured application-level input budget.

No Login Required Remote POST Denial of Service Responsible Disclosure
Summary

Executive Summary

An unauthenticated denial of service is triggered by an oversized POST body. The router web stack forwards attacker-controlled request bodies into a CGILua parser before authentication gates matter, and that parser still exposes an eager body-processing path that can be stressed from the first HTTP transaction even when the body stays below the visible default maxinput ceiling.

Root Cause

The vulnerability is not a simple missing size check. It is a pre-auth architectural flaw: the router eagerly reads and parses application/x-www-form-urlencoded input inside the allowed parser budget before authentication gates matter.

Exploit Path

  1. Oversized POST body
  2. Pre-auth parser accepts request
  3. Body is read and parsed within the configured max-input budget
  4. Router web UI becomes unavailable

Key Takeaway

Authentication is not the relevant barrier here because POST parsing happens before login enforcement, and the expensive body-processing path is still reachable below the configured size ceiling.

Trigger Request

Evidence Preview
POST / HTTP/1.1
Host: 192.168.1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 1000000

Array.from({ length: 100000 }, () => String.fromCharCode(65 + Math.floor(Math.random() * 26))).join('');

The browser-console generator shown here uses a 100,000-character body. In some models, a 1,000,000-character body was required, which can be sent using Python.

Devices

At time of report this issue was affecting 140K+ devices worldwide across 17+ models. The 140K+ number is the original 2024 exposure estimate. Based on direct testing, almost any version prior to 2022 is affected across this H-series surface.

H8102E H168N H167A H199A H288A H198A H267A H267N H268A H388X H196A H369A H268N H208N H367N H181A H196Q

Lab Validation Footage

Two clips from my 2024 lab set: one shows the denial-of-service path, the other shows a model that did not reproduce.

2024 Lab Set Live Validation H-Series
Primary Validation

Denial-of-service path

Main clip from my original 2024 validation set showing the denial-of-service path.

0:58 Primary clip
Supplementary Clip

Not vulnerable model

This clip shows a model from the same test set that did not reproduce the denial-of-service condition.

0:18 Supplementary clip

Impact and Limits

Impact

  • Web management interface becomes unavailable.
  • No login is required to reach the request path.
  • The trigger lives in request-body handling before authentication logic matters.
  • Recovery required a manual router reboot; the interface did not recover on its own during testing.

Limits

  • The 140K+ exposure figure is historical, based on the 2024 Shodan workset.
  • The vendor stated that the issue had been resolved in March 2021.

Root Cause Analysis

The extracted firmware exposes a custom httpd and a Lua web application under home/httpd. The request parser is implemented in compiled Lua chunks, including cgilua.lua and cgilua/post.lua. Those chunks were decompiled back into readable Lua source and then checked against the live validation path from the original report.

This conclusion is not based on reverse engineering alone. The issue was reported to ZTE PSIRT in May 2024 with live validation evidence, ZTE acknowledged receipt, and the later firmware analysis explains the request-body path that matches the reported behavior.

  1. cgilua.lua runs post.parsedata() for every POST before the later login and page-action flow, so the body hits parser code before authentication matters.
  2. The caller passes a visible default maxinput of 2097152 bytes into the parser.
  3. parsedata() only rejects input when inputsize > maxinput, so any body below that threshold continues into content-type dispatch.
  4. The exact dangerous branch is the x-www-form-urlencoded path: urlcode.parsequery(read(inputsize), defs.args).
  5. That is where the bug occurs: an unauthenticated attacker-controlled body is fully read and immediately parsed into Lua structures, which is enough to stall the web UI in the validated firmware set.

The vulnerability is not a simple missing size check. It is a fundamental logic flaw: the router eagerly reads and parses the entire POST body before authentication takes place, as long as the payload stays within the visible parser budget. In practice, that means a large urlencoded body can still reach a full read(inputsize) plus parsequery(...) operation on the unauthenticated request path.

Before/After Code Comparison

The H168N firmware provides a definitive look at the vulnerable parser logic. It is the clearest same-surface code comparison in the current workspace, and the decompiled source maps directly to the live exploitation path.

What The Comparison Shows

  • The H168N parser file cgilua/post.lua decompiles cleanly from both firmware artifacts compared here.
  • The decompiled Lua source is identical in the request-body path that matters for this issue.
  • cgilua.lua is also materially unchanged in the caller path that sets _default_maxinput = 2097152 and invokes post.parsedata().

Tools Used

The working set combined live validation, firmware extraction, Lua decompilation, and parser comparison. The main tools used across that path were:

  • Chrome DevTools for live request validation against the H-series test set.
  • UnluacNET for clean decompilation of the router's Lua 5.1 bytecode into readable source.
  • LuaDecompy and LuaPytecode for chunk parsing, structural checks, and disassembly support.
  • 7-Zip for extracting firmware payloads and carved filesystem regions.

Where The Bug Happens in Code

Only two files matter here. cgilua.lua is the caller that sends every POST body into the parser. cgilua/post.lua is the parser file where the urlencoded body is actually read and parsed.

cgilua.lua Reconstructed logic, not vendor source Caller-side POST setup and visible max-input default
local _default_maxinput = 2097152
local _maxinput = _default_maxinput

POST_DESC = {isExceedMaxInput = false, maxFileSize = _maxfilesize}
if requestmethod == "POST" then
  post.parsedata({
    read = sapi.Request.getpostdata,
    discardinput = ap and ap.discard_request_body,
    content_type = servervariable("CONTENT_TYPE"),
    content_length = servervariable("CONTENT_LENGTH"),
    maxinput = _maxinput,
    maxfilesize = _maxfilesize,
    args = POST,
    POST_DESC = POST_DESC
  })
end
cgilua/post.lua Reconstructed logic, not vendor source Exact branch where the bug happens
function parsedata(defs)
  assert(type(defs.args) == "table", "field `args' must be a table")
  init(defs)

  local inputsize = tonumber(defs.content_length) or 0

  if inputsize > maxinput then
    _G.g_logger:warn("inputsize(" .. inputsize .. ") > maxinput(" .. maxinput .. ")")
    defs.POST_DESC.isExceedMaxInput = true
    inputsize = 0
    return
  end

  if not content_type then
    error("Undefined Media Type")
  end

  if string.find(content_type, "x-www-form-urlencoded", 1, true) then
    urlcode.parsequery(read(inputsize), defs.args)
  elseif string.find(content_type, "multipart/form-data", 1, true) then
    if inputsize > 0 then
      Main(inputsize, defs.args, defs.POST_DESC)
    end
  elseif string.find(content_type, "application/xml", 1, true)
      or string.find(content_type, "text/xml", 1, true)
      or string.find(content_type, "text/plain", 1, true) then
    tinsert(defs.args, read(inputsize))
  else
    error("Unsupported Media Type: " .. content_type)
  end
end

The bug is the combination of those two snippets: every POST reaches post.parsedata(), and if the body is still under maxinput, the urlencoded branch performs a full read(inputsize) followed by urlcode.parsequery(...) before authentication gates matter.

Vendor Position

ZTE responded in February 2026 that the issue had been resolved on 2021-03-23 and declined vendor-side CVE assignment. The public record still matters because the issue was reported in 2024, acknowledged by the vendor, escalated through MITRE, and ultimately published as CVE-2026-34473.

Sources

Disclosure Timeline

2024-05-02

ZTE PSIRT received the original report for the unauthenticated H-series POST-body denial of service.

2024-05-06

ZTE acknowledged receipt and forwarded the report to the related product team.

2026-01-17

MITRE service request 1980204 was opened for CNA-LR processing with evidence bundles and per-issue PDF packages.

2026-02-02

ZTE declined vendor-side CVE assignment and stated that this issue had been resolved on 2021-03-23.

2026-03-27

MITRE assigned CVE-2026-34473 and asked for a public reference URL containing the minimum publication data.

2026-04-13

Service request 2016046 tracked the publication follow-up after the public advisory link was sent and ZTE was contacted again.

2026-05-06

MITRE confirmed that CVE-2026-34473 would be published on cve.org within hours, and it was published the same day.