diff --git a/guides/https.md b/guides/https.md index 0c66ee84..7483adb4 100644 --- a/guides/https.md +++ b/guides/https.md @@ -223,7 +223,7 @@ When using TLS offloading it may be necessary to make some configuration changes `Plug.SSL` takes on another important role when using TLS offloading: it can update the `:scheme` and `:port` fields in the `Plug.Conn` struct based on an HTTP header (e.g. 'X-Forwarded-Proto'), to reflect the actual protocol used by the client (HTTP or HTTPS). It is very important that the `:scheme` field properly reflects the use of HTTPS, even if the connection between the proxy and the application uses plain HTTP, because cookies set by `Plug.Session` and `Plug.Conn.put_resp_cookie/4` by default set the 'secure' cookie flag only if `:scheme` is set to `:https`! When relying on this default behaviour it is essential that `Plug.SSL` is included in the Plug pipeline, that its `:rewrite_on` option is set correctly, and that the proxy sets the appropriate header. -The `:remote_ip` field in the `Plug.Conn` struct by default contains the network peer IP address. Terminating TLS in a separate process or network element typically masks the actual client IP address from the Elixir application. If proxying is done at the HTTP layer, the original client IP address is often inserted into an HTTP header, e.g. 'X-Forwarded-For'. There are Plug packages available to extract the client IP from such a header and update the `:remote_ip` field. +The `:remote_ip` field in the `Plug.Conn` struct by default contains the network peer IP address. Terminating TLS in a separate process or network element typically masks the actual client IP address from the Elixir application. If proxying is done at the HTTP layer, the original client IP address is often inserted into an HTTP header, e.g. 'X-Forwarded-For'. There are Plugs available to extract the client IP from such a header, such as `Plug.RewriteOn`. > **Warning**: ensure that clients cannot spoof their IP address by including this header in their original request, by filtering such headers in the proxy! diff --git a/lib/plug/rewrite_on.ex b/lib/plug/rewrite_on.ex index 134bb383..e794cb50 100644 --- a/lib/plug/rewrite_on.ex +++ b/lib/plug/rewrite_on.ex @@ -10,6 +10,7 @@ defmodule Plug.RewriteOn do The supported values are: + * `:x_forwarded_for` - to override the remote ip based on on the "x-forwarded-for" header * `:x_forwarded_host` - to override the host based on on the "x-forwarded-host" header * `:x_forwarded_port` - to override the port based on on the "x-forwarded-port" header * `:x_forwarded_proto` - to override the protocol based on on the "x-forwarded-proto" header @@ -33,6 +34,12 @@ defmodule Plug.RewriteOn do def init(header), do: List.wrap(header) @impl true + def call(conn, [:x_forwarded_for | rewrite_on]) do + conn + |> put_remote_ip(get_req_header(conn, "x-forwarded-for")) + |> call(rewrite_on) + end + def call(conn, [:x_forwarded_proto | rewrite_on]) do conn |> put_scheme(get_req_header(conn, "x-forwarded-proto")) @@ -92,4 +99,14 @@ defmodule Plug.RewriteOn do _ -> conn end end + + defp put_remote_ip(conn, headers) do + with [header] <- headers, + [client | _] <- :binary.split(header, ","), + {:ok, remote_ip} <- :inet.parse_address(String.to_charlist(client)) do + %{conn | remote_ip: remote_ip} + else + _ -> conn + end + end end diff --git a/test/plug/rewrite_on_test.exs b/test/plug/rewrite_on_test.exs index 766f5c68..81efd0b0 100644 --- a/test/plug/rewrite_on_test.exs +++ b/test/plug/rewrite_on_test.exs @@ -36,7 +36,7 @@ defmodule Plug.RewriteOnTest do assert conn.port == 1234 end - test "rewrites host with a x-forwarder-host header" do + test "rewrites host with a x-forwarded-host header" do conn = conn(:get, "http://example.com/") |> put_req_header("x-forwarded-host", "truessl.example.com") @@ -45,7 +45,7 @@ defmodule Plug.RewriteOnTest do assert conn.host == "truessl.example.com" end - test "rewrites port with a x-forwarder-port header" do + test "rewrites port with a x-forwarded-port header" do conn = conn(:get, "http://example.com/") |> put_req_header("x-forwarded-port", "3030") @@ -54,6 +54,36 @@ defmodule Plug.RewriteOnTest do assert conn.port == 3030 end + test "rewrites remote_ip with a x-forwarded-for header" do + conn = + conn(:get, "http://example.com/") + |> put_req_header("x-forwarded-for", "bad") + |> call(:x_forwarded_for) + + assert conn.remote_ip == {127, 0, 0, 1} + + conn = + conn(:get, "http://example.com/") + |> put_req_header("x-forwarded-for", "4.3.2.1") + |> call(:x_forwarded_for) + + assert conn.remote_ip == {4, 3, 2, 1} + + conn = + conn(:get, "http://example.com/") + |> put_req_header("x-forwarded-for", "1.2.3.4,::1") + |> call(:x_forwarded_for) + + assert conn.remote_ip == {1, 2, 3, 4} + + conn = + conn(:get, "http://example.com/") + |> put_req_header("x-forwarded-for", "::1,1.2.3.4") + |> call(:x_forwarded_for) + + assert conn.remote_ip == {0, 0, 0, 0, 0, 0, 0, 1} + end + test "rewrites the host, the port, and the protocol" do conn = conn(:get, "http://example.com/")