CVE-2026-39807
Description
Reliance on Untrusted Inputs in a Security Decision vulnerability in mtrudel bandit allows unauthenticated transport-state spoofing on plaintext HTTP connections.
'Elixir.Bandit.Pipeline':determine_scheme/2 in lib/bandit/pipeline.ex returns the client-supplied URI scheme verbatim, ignoring the transport's secure? flag. HTTP/1.1 absolute-form request targets (e.g. GET https://victim/path HTTP/1.1) and the HTTP/2 :scheme pseudo-header are both attacker-controlled strings that flow through this function. Over a plaintext TCP connection, a client can declare https and Bandit will set conn.scheme = :https even though no TLS was negotiated.
Downstream Plug consumers that branch on conn.scheme are silently misled: Plug.SSL's already-secure branch skips its HTTP→HTTPS redirect, cookies emitted with secure: true are sent over plaintext, audit logs record requests as having arrived over HTTPS, and CSRF/SameSite gating may make incorrect decisions.
This issue affects bandit: from 1.0.0 before 1.11.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
banditHex | >= 1.0.0, < 1.11.0 | 1.11.0 |
Affected products
1Patches
13 files changed · +10 −15
lib/bandit/pipeline.ex+4 −9 modified@@ -73,22 +73,17 @@ defmodule Bandit.Pipeline do ) :: 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" @spec determine_host_and_port!(binary(), atom(), request_target(), Plug.Conn.headers()) :: {Plug.Conn.host(), Plug.Conn.port_number()}
test/bandit/http1/protocol_test.exs+2 −2 modified@@ -520,11 +520,11 @@ defmodule HTTP1ProtocolTest do end describe "absolute-form request target (RFC9112§3.2.2)" do - test "uses request-line scheme even if it does not match the transport", context do + test "uses transport scheme even if it does not match request-line scheme", context do client = SimpleHTTP1Client.tcp_client(context) SimpleHTTP1Client.send(client, "GET", "https://banana/echo_components") assert {:ok, "200 OK", _headers, body} = SimpleHTTP1Client.recv_reply(client) - assert Jason.decode!(body)["scheme"] == "https" + assert Jason.decode!(body)["scheme"] == "http" end test "derives host from the URI, even if it differs from host header", context do
test/bandit/http2/protocol_test.exs+4 −4 modified@@ -2649,7 +2649,7 @@ defmodule HTTP2ProtocolTest do assert Jason.decode!(body)["scheme"] == "https" end - test "uses :scheme even if it does not match transport", context do + test "uses transport even if it does not match :scheme", context do socket = SimpleH2Client.setup_connection(context) headers = [ @@ -2663,7 +2663,7 @@ defmodule HTTP2ProtocolTest do assert SimpleH2Client.successful_response?(socket, 1, false) {:ok, 1, true, body} = SimpleH2Client.recv_body(socket) - assert Jason.decode!(body)["scheme"] == "http" + assert Jason.decode!(body)["scheme"] == "https" end test "derives host from host header", context do @@ -2940,7 +2940,7 @@ defmodule HTTP2ProtocolTest do assert Jason.decode!(body)["scheme"] == "https" end - test "uses :scheme even if it does not match transport", context do + test "uses transport even if it does not match :scheme", context do socket = SimpleH2Client.setup_connection(context) headers = [ @@ -2954,7 +2954,7 @@ defmodule HTTP2ProtocolTest do assert SimpleH2Client.successful_response?(socket, 1, false) {:ok, 1, true, body} = SimpleH2Client.recv_body(socket) - assert Jason.decode!(body)["scheme"] == "http" + assert Jason.decode!(body)["scheme"] == "https" end test "derives host from :authority header, even if it differs from host header", context do
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-375f-4r2h-f99jghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-39807ghsaADVISORY
- cna.erlef.org/cves/CVE-2026-39807.htmlnvdWEB
- github.com/mtrudel/bandit/commit/45feea20dea8af7ffd7245271107b695c040e667nvdWEB
- github.com/mtrudel/bandit/security/advisories/GHSA-375f-4r2h-f99jnvdWEB
- osv.dev/vulnerability/EEF-CVE-2026-39807nvdWEB
News mentions
0No linked articles in our index yet.