aprs.me/test/aprsme_web/plugs/remote_ip_test.exs
Graham McIntire 0f2195ef9d
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 2s
security: CSP nonces, RemoteIp CIDR gating, rate limiting, NetworkPolicies, deadlock fixes
- Content-Security-Policy: nonce-based per-request plug replacing unsafe-inline scripts
- RemoteIp: CIDR-based trust gating via InetCidr, skips forwarded headers from untrusted peers
- Rate limiting: auth pipeline (20/min), LiveView event handlers, existing mobile channel limits
- NetworkPolicy: 4 k8s policies (web ingress, cluster, metrics, egress) for least-privilege networking
- PartitionManager: deadlock retry with exponential backoff in drop_partition
- Tests: reduced parallelism (max_cases 4), packets_test async:false to prevent trigger contention
- k8s: APRS_PASSWORD -> APRS_PASSCODE secretRef, vendor/aprs submodule hardened
2026-07-26 14:04:56 -05:00

200 lines
5.7 KiB
Elixir

defmodule AprsmeWeb.Plugs.RemoteIpTest do
use AprsmeWeb.ConnCase
alias AprsmeWeb.Plugs.RemoteIp
describe "trust gating" do
test "trusted peer (within CIDR) gets forwarded IP from X-Forwarded-For", %{conn: conn} do
opts = RemoteIp.init(trusted_proxies: ["10.0.0.0/8"])
conn =
%{conn | remote_ip: {10, 0, 0, 1}}
|> put_req_header("x-forwarded-for", "203.0.113.50")
|> RemoteIp.call(opts)
assert conn.remote_ip == {203, 0, 113, 50}
end
test "untrusted peer (outside CIDR) gets TCP peer IP, ignoring X-Forwarded-For", %{conn: conn} do
opts = RemoteIp.init(trusted_proxies: ["10.0.0.0/8"])
conn =
%{conn | remote_ip: {1, 2, 3, 4}}
|> put_req_header("x-forwarded-for", "203.0.113.50")
|> RemoteIp.call(opts)
assert conn.remote_ip == {1, 2, 3, 4}
end
test "untrusted peer ignores CF-Connecting-IP header", %{conn: conn} do
opts = RemoteIp.init(trusted_proxies: ["10.0.0.0/8"])
conn =
%{conn | remote_ip: {1, 2, 3, 4}}
|> put_req_header("cf-connecting-ip", "203.0.113.50")
|> RemoteIp.call(opts)
assert conn.remote_ip == {1, 2, 3, 4}
end
test "CF-Connecting-IP respected when peer is trusted", %{conn: conn} do
opts = RemoteIp.init(trusted_proxies: ["10.0.0.0/8"])
conn =
%{conn | remote_ip: {10, 0, 0, 1}}
|> put_req_header("cf-connecting-ip", "203.0.113.50")
|> RemoteIp.call(opts)
assert conn.remote_ip == {203, 0, 113, 50}
end
test "trusted IPv6 peer gets IPv6 from header", %{conn: conn} do
opts = RemoteIp.init(trusted_proxies: ["2001:db8::/32"])
conn =
%{conn | remote_ip: {8193, 3512, 0, 0, 0, 0, 0, 1}}
|> put_req_header("cf-connecting-ip", "2001:db8::2")
|> RemoteIp.call(opts)
assert conn.remote_ip == {8193, 3512, 0, 0, 0, 0, 0, 2}
end
test "untrusted IPv6 peer keeps its own IP", %{conn: conn} do
untrusted_ip = {0, 0, 0, 0, 0, 0, 0, 1}
opts = RemoteIp.init(trusted_proxies: ["2001:db8::/32"])
conn =
%{conn | remote_ip: untrusted_ip}
|> put_req_header("x-forwarded-for", "2001:db8::2")
|> RemoteIp.call(opts)
assert conn.remote_ip == untrusted_ip
end
test "malformed header value does not crash on untrusted peer", %{conn: conn} do
opts = RemoteIp.init(trusted_proxies: ["10.0.0.0/8"])
conn =
%{conn | remote_ip: {1, 2, 3, 4}}
|> put_req_header("cf-connecting-ip", "not-an-ip")
|> RemoteIp.call(opts)
assert conn.remote_ip == {1, 2, 3, 4}
end
test "malformed header value does not crash on trusted peer", %{conn: conn} do
opts = RemoteIp.init(trusted_proxies: ["10.0.0.0/8"])
conn =
%{conn | remote_ip: {10, 0, 0, 1}}
|> put_req_header("cf-connecting-ip", "not-an-ip")
|> RemoteIp.call(opts)
assert conn.remote_ip == {10, 0, 0, 1}
end
test "no trusted proxies configured means no peer is trusted", %{conn: conn} do
opts = RemoteIp.init(trusted_proxies: [])
conn =
%{conn | remote_ip: {10, 0, 0, 1}}
|> put_req_header("x-forwarded-for", "203.0.113.50")
|> RemoteIp.call(opts)
assert conn.remote_ip == {10, 0, 0, 1}
end
end
describe "call/2 with default trusted proxies (local host)" do
# build_conn() defaults remote_ip to {127, 0, 0, 1} which is trusted
# by the default test config (127.0.0.0/8, ::1/128).
test "sets remote_ip from CF-Connecting-IP header", %{conn: conn} do
opts = RemoteIp.init([])
conn =
conn
|> put_req_header("cf-connecting-ip", "203.0.113.50")
|> RemoteIp.call(opts)
assert conn.remote_ip == {203, 0, 113, 50}
end
test "sets remote_ip from X-Forwarded-For when CF header missing", %{conn: conn} do
opts = RemoteIp.init([])
conn =
conn
|> put_req_header("x-forwarded-for", "198.51.100.25, 10.0.0.1")
|> RemoteIp.call(opts)
assert conn.remote_ip == {198, 51, 100, 25}
end
test "prefers CF-Connecting-IP over X-Forwarded-For", %{conn: conn} do
opts = RemoteIp.init([])
conn =
conn
|> put_req_header("cf-connecting-ip", "203.0.113.50")
|> put_req_header("x-forwarded-for", "198.51.100.25")
|> RemoteIp.call(opts)
assert conn.remote_ip == {203, 0, 113, 50}
end
test "handles IPv6 addresses", %{conn: conn} do
opts = RemoteIp.init([])
conn =
conn
|> put_req_header("cf-connecting-ip", "2001:db8::1")
|> RemoteIp.call(opts)
assert conn.remote_ip == {8193, 3512, 0, 0, 0, 0, 0, 1}
end
test "leaves remote_ip unchanged when no proxy headers present", %{conn: conn} do
original_ip = conn.remote_ip
opts = RemoteIp.init([])
conn = RemoteIp.call(conn, opts)
assert conn.remote_ip == original_ip
end
test "leaves remote_ip unchanged when header contains invalid IP", %{conn: conn} do
original_ip = conn.remote_ip
opts = RemoteIp.init([])
conn =
conn
|> put_req_header("cf-connecting-ip", "not-an-ip")
|> RemoteIp.call(opts)
assert conn.remote_ip == original_ip
end
test "trims whitespace from IP addresses", %{conn: conn} do
opts = RemoteIp.init([])
conn =
conn
|> put_req_header("cf-connecting-ip", " 203.0.113.50 ")
|> RemoteIp.call(opts)
assert conn.remote_ip == {203, 0, 113, 50}
end
test "takes first IP from X-Forwarded-For chain", %{conn: conn} do
opts = RemoteIp.init([])
conn =
conn
|> put_req_header("x-forwarded-for", "203.0.113.50, 10.0.0.1, 172.16.0.1")
|> RemoteIp.call(opts)
assert conn.remote_ip == {203, 0, 113, 50}
end
end
end