@@ -22,6 +22,7 @@ module Cardano.SMASH.Server.Types (
2222 UniqueTicker (.. ),
2323 User (.. ),
2424 UserValidity (.. ),
25+ isLocalhostHost ,
2526) where
2627
2728import Cardano.Api (
@@ -44,13 +45,16 @@ import qualified Data.ByteString.Builder as BSB
4445import qualified Data.ByteString.Char8 as BS
4546import qualified Data.ByteString.Lazy as LBS
4647import Data.Swagger (NamedSchema (.. ), ToParamSchema (.. ), ToSchema (.. ))
48+ import qualified Data.Text as Text
4749import Data.Time.Clock (UTCTime )
4850import qualified Data.Time.Clock.POSIX as Time
4951import Data.Time.Format (defaultTimeLocale , formatTime , parseTimeM )
50- import Network.URI (URI , parseURI )
52+ import qualified Net.IPv4 as IPv4
53+ import qualified Net.IPv6 as IPv6
54+ import Network.URI (URI (.. ), parseURI , uriAuthority , uriRegName , uriScheme )
5155import Quiet (Quiet (.. ))
5256import Servant (FromHttpApiData (.. ), MimeUnrender (.. ), OctetStream )
53- import qualified Text.Show as Text
57+ import qualified Text.Show as TShow
5458
5559-- | The stake pool identifier. It is the hash of the stake pool operator's
5660-- vkey.
@@ -224,6 +228,52 @@ instance ToSchema PoolFetchError
224228formatTimeToNormal :: Time. POSIXTime -> Text
225229formatTimeToNormal = toS . formatTime defaultTimeLocale " %d.%m.%Y. %T" . Time. posixSecondsToUTCTime
226230
231+ isLocalhostHost :: Text -> Bool
232+ isLocalhostHost host =
233+ host == " localhost" || host == " 127.0.0.1" || host == " ::1"
234+
235+ isRestrictedIPv6 :: IPv6. IPv6 -> Bool
236+ isRestrictedIPv6 ipv6 =
237+ let (h1, _, _, _, _, _, _, _) = IPv6. toWord16s ipv6
238+ in (ipv6 == IPv6. loopback)
239+ || ((h1 .&. 0xfe00 ) == 0xfc00 )
240+ || (h1 == 0xfe80 )
241+
242+ validateSmashURL :: Text -> Maybe SmashURL
243+ validateSmashURL urlText =
244+ case parseURI (Text. unpack urlText) of
245+ Nothing -> Nothing
246+ Just uri' -> do
247+ let scheme = uriScheme uri'
248+ guard (Text. isPrefixOf " https://" urlText || (Text. isPrefixOf " http://" urlText && isLocalhostUrl urlText))
249+ guard (scheme == " http:" || scheme == " https:" )
250+ authority <- uriAuthority uri'
251+ let host = Text. pack (uriRegName authority)
252+ guard (not (isBlockedHost host))
253+ Just (SmashURL uri')
254+ where
255+ isLocalhostUrl :: Text -> Bool
256+ isLocalhostUrl u =
257+ " http://localhost" `Text.isPrefixOf` u
258+ || " http://127.0.0.1" `Text.isPrefixOf` u
259+ || " http://[::1]" `Text.isPrefixOf` u
260+
261+ isBlockedHost :: Text -> Bool
262+ isBlockedHost host
263+ | isLocalhostHost host = False
264+ | otherwise =
265+ case readMaybe (Text. unpack host) :: Maybe IPv4. IPv4 of
266+ Just ipv4 | IPv4. reserved ipv4 -> True
267+ _ ->
268+ let hostStr = Text. unpack host
269+ hostStrClean =
270+ if Text. isPrefixOf " [" host && Text. isSuffixOf " ]" host
271+ then Text. unpack (Text. init (Text. tail host))
272+ else hostStr
273+ in case readMaybe hostStrClean :: Maybe IPv6. IPv6 of
274+ Just ipv6 | isRestrictedIPv6 ipv6 -> True
275+ _ -> False
276+
227277-- | The Smash @URI@ containing remote filtering data.
228278newtype SmashURL = SmashURL { getSmashURL :: URI }
229279 deriving (Eq , Show , Generic )
@@ -237,12 +287,9 @@ instance ToJSON SmashURL where
237287instance FromJSON SmashURL where
238288 parseJSON = withObject " SmashURL" $ \ o -> do
239289 uri <- o .: " smashURL"
240-
241- let parsedURI = parseURI uri
242-
243- case parsedURI of
244- Nothing -> fail " Not a valid URI for SMASH server."
245- Just uri' -> pure (SmashURL uri')
290+ case validateSmashURL uri of
291+ Nothing -> fail " Invalid or disallowed SMASH URL. Must use HTTPS (or HTTP for localhost)."
292+ Just smashUrl -> pure smashUrl
246293
247294instance ToSchema SmashURL where
248295 declareNamedSchema _ =
@@ -391,16 +438,16 @@ instance Exception DBFail
391438instance Show DBFail where
392439 show =
393440 \ case
394- UnknownError err -> " Unknown error. Context: " <> Text .show err
441+ UnknownError err -> " Unknown error. Context: " <> TShow .show err
395442 DbInsertError err ->
396- " The database got an error while trying to insert a record. Error: " <> Text .show err
443+ " The database got an error while trying to insert a record. Error: " <> TShow .show err
397444 DbLookupPoolMetadataHash poolId poolMDHash ->
398- " The metadata with hash " <> Text .show poolMDHash <> " for pool " <> Text .show poolId <> " is missing from the DB."
399- TickerAlreadyReserved ticker -> " Ticker name " <> Text .show (getTickerName ticker) <> " is already reserved"
445+ " The metadata with hash " <> TShow .show poolMDHash <> " for pool " <> TShow .show poolId <> " is missing from the DB."
446+ TickerAlreadyReserved ticker -> " Ticker name " <> TShow .show (getTickerName ticker) <> " is already reserved"
400447 RecordDoesNotExist -> " The requested record does not exist."
401- DBFail lookupFail -> Text .show lookupFail
402- PoolDataLayerError err -> Text .show err
403- ConfigError err -> " Config Error: " <> Text .show err
448+ DBFail lookupFail -> TShow .show lookupFail
449+ PoolDataLayerError err -> TShow .show err
450+ ConfigError err -> " Config Error: " <> TShow .show err
404451
405452{-
406453
0 commit comments