CVE-2026-39807 Overview
CVE-2026-39807 affects mtrudel/bandit, an HTTP server library for Elixir, in versions 1.0.0 through 1.11.0 (exclusive). The flaw lets an unauthenticated client spoof the connection's transport state on plaintext HTTP. The Elixir.Bandit.Pipeline.determine_scheme/2 function in lib/bandit/pipeline.ex returns the client-supplied URI scheme verbatim and ignores the transport's secure? flag. Attackers can send an HTTP/1.1 absolute-form target such as GET https://victim/path HTTP/1.1 or set the HTTP/2 :scheme pseudo-header to https over a plain TCP connection. Bandit then sets conn.scheme = :https, misleading downstream Plug consumers. The weakness maps to [CWE-807].
Critical Impact
Downstream Plug components branching on conn.scheme skip HTTP→HTTPS redirects, emit secure: true cookies over plaintext, log requests as HTTPS, and can make incorrect CSRF or SameSite decisions.
Affected Products
- mtrudel bandit versions >= 1.0.0 and < 1.11.0
- Elixir/Plug applications relying on conn.scheme for security decisions
- HTTP/1.1 and HTTP/2 endpoints served by vulnerable Bandit releases
Discovery Timeline
- 2026-05-01 - CVE-2026-39807 published to NVD
- 2026-05-05 - Last updated in NVD database
Technical Details for CVE-2026-39807
Vulnerability Analysis
Bandit's request pipeline builds the Plug.Conn URI by calling determine_scheme/2. The original implementation prefers the scheme supplied by the client over the actual transport state. When the client omits a scheme, the function falls back to secure?. When the client supplies one, the function returns it unchanged. This trust placement violates the security decision model required when deriving transport identity. The vulnerability is classified under [CWE-807] (Reliance on Untrusted Inputs in a Security Decision).
Root Cause
The root cause lives in lib/bandit/pipeline.ex. The function signature determine_scheme(secure?, {scheme, _, _, _}) uses pattern matching that returns the attacker-controlled scheme whenever it is non-nil. HTTP/1.1 absolute-form request targets and the HTTP/2 :scheme pseudo-header both populate that field with untrusted input. The transport's authoritative secure? boolean is consulted only when the client omits the value.
Attack Vector
An unauthenticated remote attacker opens a plaintext TCP connection to the Bandit server. Over HTTP/1.1, the attacker issues an absolute-form request line such as GET https://victim/path HTTP/1.1. Over HTTP/2, the attacker sets the :scheme pseudo-header to https. Bandit assigns conn.scheme = :https, and any Plug middleware that branches on the scheme treats the plaintext request as TLS-secured. Plug.SSL skips its redirect, cookies marked secure: true traverse cleartext, and audit logs record HTTPS access.
# Patch in lib/bandit/pipeline.ex
) :: Plug.Conn.t()
defp build_conn!(transport, method, request_target, headers, {secure?, peer_address}, opts) do
adapter = Bandit.Adapter.init(self(), transport, method, headers, opts)
- scheme = determine_scheme(secure?, request_target)
+ scheme = determine_scheme(secure?)
version = Bandit.HTTPTransport.version(transport)
{host, port} = determine_host_and_port!(scheme, version, request_target, headers)
{path, query} = determine_path_and_query(request_target)
uri = %URI{scheme: scheme, host: host, port: port, path: path, query: query}
Plug.Conn.Adapter.conn({Bandit.Adapter, adapter}, method, uri, peer_address, headers)
end
- @spec determine_scheme(boolean(), request_target()) :: String.t() | nil
- defp determine_scheme(secure?, {scheme, _, _, _}) do
- case {secure?, scheme} do
- {true, nil} -> "https"
- {false, nil} -> "http"
- {_, scheme} -> scheme
- end
- end
+ @spec determine_scheme(boolean()) :: String.t()
+ defp determine_scheme(true), do: "https"
+ defp determine_scheme(false), do: "http"
Source: GitHub Commit 45feea2. The fix removes the client-controlled scheme parameter and derives the value solely from the transport's secure? flag.
Detection Methods for CVE-2026-39807
Indicators of Compromise
- HTTP/1.1 requests on plaintext listeners with absolute-form targets that begin with https:// (for example GET https://host/path HTTP/1.1).
- HTTP/2 requests on plaintext listeners carrying a :scheme pseudo-header value of https.
- Cookies with the Secure attribute observed in unencrypted packet captures of traffic to Bandit endpoints.
- Application audit logs recording scheme=https for connections whose listener port serves cleartext.
Detection Strategies
- Inspect Bandit access logs and Plug telemetry events for mismatches between the listening port's TLS state and the recorded conn.scheme.
- Run network sensors that flag absolute-form HTTP/1.1 request lines arriving on non-TLS sockets.
- Decode HTTP/2 frames on plaintext listeners and alert when :scheme equals https.
Monitoring Recommendations
- Forward web server, reverse proxy, and Plug telemetry to a centralized analytics platform and correlate by listener port versus reported scheme.
- Track issuance of Set-Cookie headers with the Secure flag on plaintext sockets as a high-fidelity signal of exploitation.
- Alert on Plug.SSL traversals where the redirect path was bypassed for clients that did not negotiate TLS.
How to Mitigate CVE-2026-39807
Immediate Actions Required
- Upgrade mtrudel/bandit to version 1.11.0 or later in all Elixir applications using the library.
- Audit deployments for plaintext Bandit listeners that are exposed to untrusted networks.
- Review application logs for prior requests where conn.scheme was https on plaintext-only ports.
Patch Information
The maintainer fixed the issue in Bandit 1.11.0 via commit 45feea20dea8af7ffd7245271107b695c040e667. The patch redefines determine_scheme/1 to take only the transport secure? boolean, eliminating reliance on the client-supplied scheme. Refer to the GitHub Security Advisory GHSA-375f-4r2h-f99j, the CNA advisory at erlef.org, and the OSV record EEF-CVE-2026-39807.
Workarounds
- Terminate TLS at an upstream proxy and force conn.scheme based on a trusted header set by that proxy rather than Bandit's pipeline.
- Restrict Bandit's plaintext listener to localhost or an internal segment so untrusted clients cannot reach it directly.
- Add a Plug early in the pipeline that overrides conn.scheme using the listener's transport state until the upgrade is applied.
# Update bandit dependency in mix.exs to the patched release
# mix.exs
# defp deps do
# [{:bandit, "~> 1.11"}]
# end
mix deps.update bandit
mix deps.get
mix compile
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.


