espial-0.0.41: src/Network/PrivateAddress.hs
-- | Guards against SSRF when the server fetches a user-supplied URL on the
-- user's behalf (e.g. title lookups): resolves the host and rejects it if
-- any resolved address falls in a private, loopback, link-local, or other
-- non-public range.
module Network.PrivateAddress
( isDisallowedFetchHost,
isDisallowedFetchUrl,
)
where
import ClassyPrelude
import Data.IP (AddrRange, IP (..), IPv4, IPv6, fromSockAddr, isMatchedTo)
import Network.HTTP.Client qualified as NH
import Network.Socket (AddrInfo (addrAddress), HostName, defaultHints, getAddrInfo)
isDisallowedFetchHost :: HostName -> IO Bool
isDisallowedFetchHost host = do
addrs <- getAddrInfo (Just defaultHints) (Just host) Nothing `catch` \(_ :: SomeException) -> pure []
pure $ null addrs || any (maybe True (isPrivateOrReservedIP . fst) . fromSockAddr . addrAddress) addrs
isDisallowedFetchUrl :: Text -> IO Bool
isDisallowedFetchUrl rawUrl =
( do
req <- NH.parseRequest (unpack rawUrl)
isDisallowedFetchHost (unpack (decodeUtf8 (NH.host req)))
)
`catch` \(_ :: SomeException) -> pure True
isPrivateOrReservedIP :: IP -> Bool
isPrivateOrReservedIP (IPv4 ip) = any (ip `isMatchedTo`) privateIPv4Ranges
isPrivateOrReservedIP (IPv6 ip) = any (ip `isMatchedTo`) privateIPv6Ranges
privateIPv4Ranges :: [AddrRange IPv4]
privateIPv4Ranges =
[ "0.0.0.0/8", -- "this" network
"10.0.0.0/8", -- private
"100.64.0.0/10", -- carrier-grade NAT
"127.0.0.0/8", -- loopback
"169.254.0.0/16", -- link-local (incl. cloud metadata endpoints)
"172.16.0.0/12", -- private
"192.0.0.0/24", -- IETF protocol assignments
"192.0.2.0/24", -- documentation (TEST-NET-1)
"192.168.0.0/16", -- private
"198.18.0.0/15", -- benchmarking
"198.51.100.0/24", -- documentation (TEST-NET-2)
"203.0.113.0/24", -- documentation (TEST-NET-3)
"224.0.0.0/4", -- multicast
"240.0.0.0/4", -- reserved
"255.255.255.255/32" -- broadcast
]
privateIPv6Ranges :: [AddrRange IPv6]
privateIPv6Ranges =
[ "::1/128", -- loopback
"::/128", -- unspecified
"::ffff:0:0/96", -- IPv4-mapped (denylisted wholesale rather than unwrapped)
"64:ff9b::/96", -- NAT64
"100::/64", -- discard-only
"2001:db8::/32", -- documentation
"fc00::/7", -- unique local (private)
"fe80::/10", -- link-local
"ff00::/8" -- multicast
]