VYPR
Medium severityNVD Advisory· Published May 1, 2026· Updated May 5, 2026

CVE-2026-39807

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.

PackageAffected versionsPatched versions
banditHex
>= 1.0.0, < 1.11.01.11.0

Affected products

1

Patches

1
45feea20dea8

Merge commit from fork

https://github.com/mtrudel/banditMat TrudelMay 1, 2026via ghsa
3 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

News mentions

0

No linked articles in our index yet.