towerops/lib/towerops_web/remote_ip.ex
Graham McIntire 3632580d1b fix: address security and reliability findings from bugs.md review
- Client IP: only trust X-Forwarded-For from RFC 1918 proxy IPs
- Webhook auth: handle nil/blank secret with controlled error, not 500 crash
- Sudo redirect: reuse validated return_path? from login to prevent open redirect
- Map live: remove redundant inline script (ensureLeaflet hook handles loading)
- Bang calls: convert crash-prone exact matches to case in QR live and API controllers
2026-05-11 18:54:12 -05:00

62 lines
1.9 KiB
Elixir

defmodule ToweropsWeb.RemoteIp do
@moduledoc """
Shared helpers for extracting and formatting remote IP addresses.
"""
import Plug.Conn, only: [get_req_header: 2]
@spec from_conn(Plug.Conn.t()) :: String.t() | nil
def from_conn(conn) do
if trusted_proxy?(conn.remote_ip) do
case get_req_header(conn, "x-forwarded-for") do
[forwarded | _] ->
forwarded
|> String.split(",")
|> List.first()
|> String.trim()
[] ->
from_x_real_ip_or_tuple(conn)
end
else
format(conn.remote_ip)
end
end
@spec from_socket(Phoenix.Socket.t()) :: String.t() | nil
def from_socket(socket) do
# Use peer_data stashed into socket assigns at connect time via
# `connect_info: [:peer_data]`. This is the documented Phoenix API
# and is adapter-agnostic (works with both Bandit and Cowboy).
case socket.assigns[:peer_data] do
%{address: ip} when is_tuple(ip) -> format(ip)
_ -> nil
end
end
@spec format(tuple() | nil) :: String.t() | nil
def format({a, b, c, d}), do: "#{a}.#{b}.#{c}.#{d}"
def format({a, b, c, d, e, f, g, h}) do
Enum.map_join([a, b, c, d, e, f, g, h], ":", &(&1 |> Integer.to_string(16) |> String.upcase()))
end
def format(_), do: nil
defp from_x_real_ip_or_tuple(conn) do
case get_req_header(conn, "x-real-ip") do
[real_ip | _] -> real_ip
[] -> format(conn.remote_ip)
end
end
# Only trust forwarding headers from known proxy/RFC 1918 addresses.
# Direct internet clients can spoof x-forwarded-for, so we must not
# trust it unless the immediate peer is a known reverse proxy.
defp trusted_proxy?({10, _b, _c, _d}), do: true
defp trusted_proxy?({172, b, _c, _d}) when b >= 16 and b <= 31, do: true
defp trusted_proxy?({192, 168, _c, _d}), do: true
defp trusted_proxy?({127, _b, _c, _d}), do: true
defp trusted_proxy?({0xFD00, _, _, _, _, _, _, _}), do: true
defp trusted_proxy?(_), do: false
end