@@ -7,6 +7,7 @@ module Cardano.DbSync.OffChain.Http (
77 httpGetOffChainVoteData ,
88 parseAndValidateVoteData ,
99 parseOffChainUrl ,
10+ newRestrictedManager ,
1011) where
1112
1213import qualified Cardano.Crypto.Hash.Blake2b as Crypto
@@ -38,8 +39,9 @@ import qualified Data.Text.Encoding as Text
3839import GHC.Show (show )
3940import Network.HTTP.Client (HttpException (.. ))
4041import qualified Network.HTTP.Client as Http
41- import Network.HTTP.Client.TLS ( tlsManagerSettings )
42+ import Network.HTTP.Client.Restricted ( Restriction , addressRestriction , connectionRestricted , mkRestrictedManagerSettings )
4243import qualified Network.HTTP.Types as Http
44+ import qualified Network.Socket as Socket
4345
4446-------------------------------------------------------------------------------------
4547-- Get OffChain data
@@ -103,7 +105,7 @@ httpGetOffChainVoteDataSingle ::
103105 DB. AnchorType ->
104106 ExceptT OffChainFetchError IO SimplifiedOffChainVoteData
105107httpGetOffChainVoteDataSingle vurl metaHash anchorType = do
106- manager <- liftIO $ Http. newManager tlsManagerSettings
108+ manager <- liftIO newRestrictedManager
107109 request <- parseOffChainUrl url
108110 let req = httpGetBytes manager request 3000000 3000000 url
109111 httpRes <- handleExceptT (convertHttpException url) req
@@ -216,8 +218,20 @@ isPossiblyJsonObject bs =
216218-- Url
217219-------------------------------------------------------------------------------------
218220parseOffChainUrl :: OffChainUrlType -> ExceptT OffChainFetchError IO Http. Request
219- parseOffChainUrl url =
220- handleExceptT wrapHttpException $ applyContentType <$> Http. parseRequest (showUrl url)
221+ parseOffChainUrl url = do
222+ let urlText = Text. pack $ showUrl url
223+ unless (Text. isPrefixOf " https://" urlText || Text. isPrefixOf " http://" urlText) $
224+ left $
225+ OCFErrUrlParseFail url " Only HTTP/HTTPS URLs are allowed"
226+ request <- handleExceptT wrapHttpException $ Http. parseRequest (showUrl url)
227+ unless (Http. method request == " GET" ) $
228+ left $
229+ OCFErrUrlParseFail url " Only GET requests are allowed"
230+ let hostBS = Http. host request
231+ when (isLocalhostHost hostBS) $
232+ left $
233+ OCFErrUrlParseFail url " Access to localhost is not allowed"
234+ pure $ applyContentType request
221235 where
222236 wrapHttpException :: HttpException -> OffChainFetchError
223237 wrapHttpException err = OCFErrHttpException url (textShow err)
@@ -250,8 +264,56 @@ convertHttpException url he =
250264 OffChainPoolUrl _ -> OCFErrUrlParseFail (OffChainPoolUrl $ PoolUrl $ Text. pack urlx) (Text. pack err)
251265 OffChainVoteUrl _ -> OCFErrUrlParseFail (OffChainVoteUrl $ VoteUrl $ Text. pack urlx) (Text. pack err)
252266
267+ isLocalhostHost :: ByteString -> Bool
268+ isLocalhostHost host =
269+ host == " localhost"
270+ || host == " 127.0.0.1"
271+ || host == " ::1"
272+ || host == " [::1]"
273+ || BS. isPrefixOf " 10." host
274+ || BS. isPrefixOf " 192.168." host
275+
253276useIpfsGatewayMaybe :: VoteUrl -> [Text ] -> Maybe [VoteUrl ]
254277useIpfsGatewayMaybe vu gateways =
255278 case Text. stripPrefix " ipfs://" (unVoteUrl vu) of
256279 Just sf -> Just $ VoteUrl . (<> sf) <$> gateways
257280 Nothing -> Nothing
281+
282+ -------------------------------------------------------------------------------------
283+ -- Restricted Manager
284+ -------------------------------------------------------------------------------------
285+
286+ -- | Create a restricted 'Http.ManagerSettings' that blocks connections to
287+ -- private, loopback, and link-local IP addresses. The restriction is
288+ -- checked at connect time on the resolved IP, so it applies to redirects
289+ -- and prevents DNS rebinding attacks.
290+ newRestrictedManager :: IO Http. Manager
291+ newRestrictedManager = do
292+ (settings, _mProxyRestricted) <- mkRestrictedManagerSettings offchainRestriction Nothing Nothing
293+ Http. newManager settings
294+
295+ offchainRestriction :: Restriction
296+ offchainRestriction = addressRestriction $ \ addr ->
297+ if isPrivateAddr (Socket. addrAddress addr)
298+ then Just $ connectionRestricted (" Access to private, loopback, or link-local IP address is not allowed: " ++ ) addr
299+ else Nothing
300+
301+ isPrivateAddr :: Socket. SockAddr -> Bool
302+ isPrivateAddr (Socket. SockAddrInet _ hostAddr) =
303+ let (a, b, _, _) = Socket. hostAddressToTuple hostAddr
304+ in a == 0 -- 0.0.0.0/8 (current network)
305+ || a == 10 -- 10.0.0.0/8 (private)
306+ || (a == 100 && b >= 64 && b <= 127 ) -- 100.64.0.0/10 (CGNAT)
307+ || a == 127 -- 127.0.0.0/8 (loopback)
308+ || (a == 169 && b == 254 ) -- 169.254.0.0/16 (link-local)
309+ || (a == 172 && b >= 16 && b <= 31 ) -- 172.16.0.0/12 (private)
310+ || (a == 192 && b == 168 ) -- 192.168.0.0/16 (private)
311+ || (a == 198 && b >= 18 && b <= 19 ) -- 198.18.0.0/15 (benchmarking)
312+ || a >= 224 -- 224.0.0.0+ (multicast + reserved + broadcast)
313+ isPrivateAddr (Socket. SockAddrInet6 _ _ hostAddr6 _) =
314+ let addr@ (w1, _, _, _, _, _, _, _) = Socket. hostAddress6ToTuple hostAddr6
315+ in addr == (0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ) -- ::
316+ || addr == (0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 ) -- ::1
317+ || (w1 .&. 0xFE00 ) == 0xFC00 -- fc00::/7 (ULA)
318+ || (w1 .&. 0xFFC0 ) == 0xFE80 -- fe80::/10 (link-local)
319+ isPrivateAddr _ = False
0 commit comments