The remote_ip plug previously trusted Cf-Connecting-Ip and X-Forwarded-For on every request, so any client reaching a pod directly (cluster-internal, kubectl port-forward, misconfigured Service) could spoof conn.remote_ip, which flows into session storage and logs. Now only honour those headers when the immediate TCP peer sits inside a configured CIDR list. Default list covers loopback, RFC1918, CGNAT, link-local, and IPv6 ULA — matches the k8s pod/service network. Override via config :microwaveprop, :trusted_proxies, or per-environment via the TRUSTED_PROXY_CIDRS env var in runtime.exs. Also refresh the stale "nginx/dokku proxy" moduledoc — deployment has been k8s + Cloudflare for a while.
220 lines
5.8 KiB
Elixir
220 lines
5.8 KiB
Elixir
defmodule MicrowavepropWeb.Plugs.RemoteIpTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
import Plug.Conn
|
|
import Plug.Test
|
|
|
|
alias MicrowavepropWeb.Plugs.RemoteIp
|
|
|
|
# A trusted peer that looks like a k8s cluster pod or ingress.
|
|
@trusted_peer {10, 42, 0, 1}
|
|
# An external peer: must NOT be able to spoof headers.
|
|
@untrusted_peer {8, 8, 8, 8}
|
|
|
|
defp call_plug(conn, opts \\ []) do
|
|
RemoteIp.call(conn, RemoteIp.init(opts))
|
|
end
|
|
|
|
defp with_peer(conn, ip) do
|
|
Map.put(conn, :remote_ip, ip)
|
|
end
|
|
|
|
describe "call/2 with trusted peer" do
|
|
test "uses cf-connecting-ip when immediate peer is trusted" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@trusted_peer)
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == {1, 2, 3, 4}
|
|
end
|
|
|
|
test "falls back to x-forwarded-for when no cf-connecting-ip (trusted peer)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@trusted_peer)
|
|
|> put_req_header("x-forwarded-for", "5.6.7.8")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == {5, 6, 7, 8}
|
|
end
|
|
|
|
test "prefers cf-connecting-ip over x-forwarded-for (trusted peer)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@trusted_peer)
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> put_req_header("x-forwarded-for", "5.6.7.8")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == {1, 2, 3, 4}
|
|
end
|
|
|
|
test "takes first IP from comma-separated x-forwarded-for (trusted peer)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@trusted_peer)
|
|
|> put_req_header("x-forwarded-for", "10.0.0.1, 10.0.0.2, 10.0.0.3")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == {10, 0, 0, 1}
|
|
end
|
|
|
|
test "preserves original remote_ip when no headers present (trusted peer)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@trusted_peer)
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == @trusted_peer
|
|
end
|
|
|
|
test "handles IPv6 header value (trusted peer)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@trusted_peer)
|
|
|> put_req_header("cf-connecting-ip", "2001:db8::1")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == {8193, 3512, 0, 0, 0, 0, 0, 1}
|
|
end
|
|
|
|
test "handles loopback peer (IPv4)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer({127, 0, 0, 1})
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == {1, 2, 3, 4}
|
|
end
|
|
|
|
test "handles loopback peer (IPv6 ::1)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer({0, 0, 0, 0, 0, 0, 0, 1})
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == {1, 2, 3, 4}
|
|
end
|
|
|
|
test "ignores malformed IP in header (trusted peer)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@trusted_peer)
|
|
|> put_req_header("cf-connecting-ip", "not-an-ip")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == @trusted_peer
|
|
end
|
|
end
|
|
|
|
describe "call/2 with untrusted peer" do
|
|
test "ignores cf-connecting-ip from untrusted peer (spoof attempt)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@untrusted_peer)
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == @untrusted_peer
|
|
end
|
|
|
|
test "ignores x-forwarded-for from untrusted peer (spoof attempt)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@untrusted_peer)
|
|
|> put_req_header("x-forwarded-for", "1.2.3.4, 5.6.7.8")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == @untrusted_peer
|
|
end
|
|
|
|
test "ignores both spoofed headers from untrusted peer" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@untrusted_peer)
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> put_req_header("x-forwarded-for", "9.9.9.9")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == @untrusted_peer
|
|
end
|
|
end
|
|
|
|
describe "call/2 with IPv6 peers" do
|
|
test "trusts IPv6 ULA peer when configured" do
|
|
# fc00::/7 ULA range — included in default trusted list.
|
|
peer = {0xFC00, 0, 0, 0, 0, 0, 0, 1}
|
|
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(peer)
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug(trusted_proxies: ["fc00::/7"])
|
|
|
|
assert conn.remote_ip == {1, 2, 3, 4}
|
|
end
|
|
|
|
test "does not trust a public IPv6 peer when only ULA is trusted" do
|
|
peer = {0x2001, 0xDB8, 0, 0, 0, 0, 0, 1}
|
|
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(peer)
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug(trusted_proxies: ["fc00::/7"])
|
|
|
|
assert conn.remote_ip == peer
|
|
end
|
|
end
|
|
|
|
describe "call/2 with custom trusted_proxies option" do
|
|
test "explicit empty list trusts nothing — even loopback is ignored" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer({127, 0, 0, 1})
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug(trusted_proxies: [])
|
|
|
|
assert conn.remote_ip == {127, 0, 0, 1}
|
|
end
|
|
|
|
test "single-host CIDR (/32) trusts only that exact IP" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer({203, 0, 113, 5})
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug(trusted_proxies: ["203.0.113.5/32"])
|
|
|
|
assert conn.remote_ip == {1, 2, 3, 4}
|
|
|
|
conn2 =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer({203, 0, 113, 6})
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug(trusted_proxies: ["203.0.113.5/32"])
|
|
|
|
assert conn2.remote_ip == {203, 0, 113, 6}
|
|
end
|
|
end
|
|
end
|