Skip to content

Commit

Permalink
Support x-forwarded-for
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed May 14, 2024
1 parent bd8a574 commit 0574733
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 3 deletions.
2 changes: 1 addition & 1 deletion guides/https.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
17 changes: 17 additions & 0 deletions lib/plug/rewrite_on.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"))
Expand Down Expand Up @@ -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
34 changes: 32 additions & 2 deletions test/plug/rewrite_on_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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/")
Expand Down

0 comments on commit 0574733

Please sign in to comment.