From 72f4a921f218b12806bfb71d3a34e10296744721 Mon Sep 17 00:00:00 2001 From: Wleeaf <179125857+wleeaf@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:08:01 +0000 Subject: [PATCH] Split the port off x-forwarded-host in Plug.RewriteOn Proxies often send the port with the host, as in "x-forwarded-host: example.com:8443" (nginx's $http_host, or $host:$server_port). Plug.RewriteOn put the whole value into conn.host, so conn.host became "example.com:8443" while conn.port kept the port of the connection to the proxy, and URLs built from them were wrong. The port is now split off into conn.port, like the Host header. IPv6 literals keep their brackets ("[::1]:8443" gives "[::1]" and 8443), and a value without a valid port is used as the host as before. An x-forwarded-port header listed after :x_forwarded_host still overrides the port. --- lib/plug/rewrite_on.ex | 30 ++++++++++++++++++++--- test/plug/rewrite_on_test.exs | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/lib/plug/rewrite_on.ex b/lib/plug/rewrite_on.ex index 110b0df19..2c5b079d0 100644 --- a/lib/plug/rewrite_on.ex +++ b/lib/plug/rewrite_on.ex @@ -11,14 +11,15 @@ defmodule Plug.RewriteOn do The supported values are: * `:x_forwarded_for` - to override the remote IP based on the "x-forwarded-for" header - * `:x_forwarded_host` - to override the host based on the "x-forwarded-host" header + * `:x_forwarded_host` - to override the host based on the "x-forwarded-host" header, + and the port if the header includes one * `:x_forwarded_port` - to override the port based on the "x-forwarded-port" header * `:x_forwarded_proto` - to override the protocol based on the "x-forwarded-proto" header Some HTTPS proxies use nonstandard headers, which can be specified in the list via tuples: * `{:remote_ip, header}` - to override the remote IP based on a custom header - * `{:host, header}` - to override the host based on a custom header + * `{:host, header}` - to override the host (and port) based on a custom header * `{:port, header}` - to override the port based on a custom header * `{:scheme, header}` - to override the protocol based on a custom header @@ -114,12 +115,33 @@ defmodule Plug.RewriteOn do defp put_scheme(conn, _scheme), do: conn - defp put_host(conn, [proper_host]), - do: %{conn | host: proper_host} + defp put_host(conn, [proper_host]) do + case split_port(proper_host) do + {host, port} -> %{conn | host: host, port: port} + :error -> %{conn | host: proper_host} + end + end defp put_host(conn, _), do: conn + # The header may include the port, as in "example.com:8443" or "[::1]:8443" + defp split_port(host_port) do + with [_ | _] = colons <- :binary.matches(host_port, ":"), + {pos, 1} when pos > 0 <- List.last(colons), + true <- length(colons) == 1 or bracketed_ipv6?(host_port, pos), + port = binary_part(host_port, pos + 1, byte_size(host_port) - pos - 1), + true <- byte_size(port) in 1..5, + {port, ""} <- Integer.parse(port) do + {binary_part(host_port, 0, pos), port} + else + _ -> :error + end + end + + defp bracketed_ipv6?(host_port, pos), + do: :binary.first(host_port) == ?[ and :binary.at(host_port, pos - 1) == ?] + defp put_port(conn, headers) do with [header] <- headers, true <- byte_size(header) <= 5, diff --git a/test/plug/rewrite_on_test.exs b/test/plug/rewrite_on_test.exs index 79db53a8f..a11898fa0 100644 --- a/test/plug/rewrite_on_test.exs +++ b/test/plug/rewrite_on_test.exs @@ -66,6 +66,52 @@ defmodule Plug.RewriteOnTest do assert conn.host == "truessl.example.com" end + test "rewrites host and port with a x-forwarded-host header that has a port" do + conn = + conn(:get, "http://example.com/") + |> put_req_header("x-forwarded-host", "truessl.example.com:8443") + |> call(:x_forwarded_host) + + assert conn.host == "truessl.example.com" + assert conn.port == 8443 + + conn = + conn(:get, "http://example.com/") + |> put_req_header("x-forwarded-host", "[2001:db8::1]:8443") + |> call(:x_forwarded_host) + + assert conn.host == "[2001:db8::1]" + assert conn.port == 8443 + end + + test "rewrites only the host with a x-forwarded-host header without a valid port" do + for host <- [ + "[2001:db8::1]", + "2001:db8::1", + "truessl.example.com:", + "truessl.example.com:https" + ] do + conn = + conn(:get, "http://example.com/") + |> put_req_header("x-forwarded-host", host) + |> call(:x_forwarded_host) + + assert conn.host == host + assert conn.port == 80 + end + end + + test "x-forwarded-port takes precedence over the port in x-forwarded-host" do + conn = + conn(:get, "http://example.com/") + |> put_req_header("x-forwarded-host", "truessl.example.com:8443") + |> put_req_header("x-forwarded-port", "9443") + |> call([:x_forwarded_host, :x_forwarded_port]) + + assert conn.host == "truessl.example.com" + assert conn.port == 9443 + end + test "rewrites host with a custom header" do conn = conn(:get, "http://example.com/")