Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent HashDOS #138

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions http-api-data.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ library
, text-iso8601 >= 0.1 && < 0.2
, tagged >= 0.8.5 && < 0.9
, time-compat >= 1.9.5 && < 1.10
, unordered-containers >= 0.2.10.0 && < 0.3
, uuid-types >= 1.0.3 && < 1.1

if flag(use-text-show)
Expand Down Expand Up @@ -92,7 +91,7 @@ test-suite spec
, http-api-data
, text
, time-compat
, unordered-containers
, containers
, uuid-types

build-depends: HUnit >= 1.6.0.0 && <1.7
Expand Down
27 changes: 9 additions & 18 deletions src/Web/Internal/FormUrlEncoded.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,11 @@ import Data.Coerce (coerce)
import qualified Data.Foldable as F
import Data.Functor.Identity (Identity(Identity))
import Data.Hashable (Hashable)
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Map.Strict as Map
import Data.Int (Int16, Int32, Int64, Int8)
import Data.IntMap (IntMap)
import qualified Data.IntMap as IntMap
import Data.List (intersperse, sortBy)
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Monoid (All (..), Any (..), Dual (..),
Product (..), Sum (..))
import Data.Ord (comparing)
Expand Down Expand Up @@ -205,7 +202,7 @@ instance FromFormKey Natural where parseFormKey = parseQueryParam
-- | The contents of a form, not yet URL-encoded.
--
-- 'Form' can be URL-encoded with 'urlEncodeForm' and URL-decoded with 'urlDecodeForm'.
newtype Form = Form { unForm :: HashMap Text [Text] }
newtype Form = Form { unForm :: Map.Map Text [Text] }
deriving (Eq, Read, Generic, Semigroup, Monoid)

instance Show Form where
Expand All @@ -216,8 +213,8 @@ instance Show Form where
-- For a stable conversion use 'toListStable'.
instance IsList Form where
type Item Form = (Text, Text)
fromList = Form . HashMap.fromListWith (flip (<>)) . fmap (\(k, v) -> (k, [v]))
toList = concatMap (\(k, vs) -> map ((,) k) vs) . HashMap.toList . unForm
fromList = Form . Map.fromListWith (flip (<>)) . fmap (\(k, v) -> (k, [v]))
toList = concatMap (\(k, vs) -> map ((,) k) vs) . Map.toList . unForm

-- | A stable version of 'toList'.
toListStable :: Form -> [(Text, Text)]
Expand Down Expand Up @@ -270,12 +267,9 @@ instance ToForm Form where toForm = id
instance (ToFormKey k, ToHttpApiData v) => ToForm [(k, v)] where
toForm = fromList . map (toFormKey *** toQueryParam)

instance (ToFormKey k, ToHttpApiData v) => ToForm (Map k [v]) where
instance (ToFormKey k, ToHttpApiData v) => ToForm (Map.Map k [v]) where
toForm = fromEntriesByKey . Map.toList

instance (ToFormKey k, ToHttpApiData v) => ToForm (HashMap k [v]) where
toForm = fromEntriesByKey . HashMap.toList

instance ToHttpApiData v => ToForm (IntMap [v]) where
toForm = fromEntriesByKey . IntMap.toList

Expand All @@ -284,7 +278,7 @@ instance ToHttpApiData v => ToForm (IntMap [v]) where
-- >>> fromEntriesByKey [("name",["Nick"]),("color",["red","blue"])]
-- fromList [("color","red"),("color","blue"),("name","Nick")]
fromEntriesByKey :: (ToFormKey k, ToHttpApiData v) => [(k, [v])] -> Form
fromEntriesByKey = Form . HashMap.fromListWith (<>) . map (toFormKey *** map toQueryParam)
fromEntriesByKey = Form . Map.fromListWith (<>) . map (toFormKey *** map toQueryParam)

data Proxy3 a b c = Proxy3

Expand Down Expand Up @@ -417,12 +411,9 @@ instance FromForm Form where fromForm = pure
instance (FromFormKey k, FromHttpApiData v) => FromForm [(k, v)] where
fromForm = fmap (concatMap (\(k, vs) -> map ((,) k) vs)) . toEntriesByKey

instance (Ord k, FromFormKey k, FromHttpApiData v) => FromForm (Map k [v]) where
instance (Ord k, Eq k, Hashable k, FromFormKey k, FromHttpApiData v) => FromForm (Map.Map k [v]) where
fromForm = fmap (Map.fromListWith (<>)) . toEntriesByKey

instance (Eq k, Hashable k, FromFormKey k, FromHttpApiData v) => FromForm (HashMap k [v]) where
fromForm = fmap (HashMap.fromListWith (<>)) . toEntriesByKey

instance FromHttpApiData v => FromForm (IntMap [v]) where
fromForm = fmap (IntMap.fromListWith (<>)) . toEntriesByKey

Expand All @@ -431,7 +422,7 @@ instance FromHttpApiData v => FromForm (IntMap [v]) where
-- _NOTE:_ this conversion is unstable and may result in different key order
-- (but not values). For a stable encoding see 'toEntriesByKeyStable'.
toEntriesByKey :: (FromFormKey k, FromHttpApiData v) => Form -> Either Text [(k, [v])]
toEntriesByKey = traverse parseGroup . HashMap.toList . unForm
toEntriesByKey = traverse parseGroup . Map.toList . unForm
where
parseGroup (k, vs) = (,) <$> parseFormKey k <*> traverse parseQueryParam vs

Expand Down Expand Up @@ -658,7 +649,7 @@ urlEncodeAsFormStable = urlEncodeFormStable . toForm
-- >>> lookupAll "name" [("name", "Oleg"), ("name", "David")]
-- ["Oleg","David"]
lookupAll :: Text -> Form -> [Text]
lookupAll key = F.concat . HashMap.lookup key . unForm
lookupAll key = F.concat . Map.lookup key . unForm

-- | Lookup an optional value for a key.
-- Fail if there is more than one value.
Expand Down
3 changes: 1 addition & 2 deletions src/Web/Internal/HttpApiData.hs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@ import qualified Data.Fixed as F
import Data.Functor.Identity (Identity(Identity))
import Data.Int (Int16, Int32, Int64, Int8)
import Data.Kind (Type)
import qualified Data.Map as Map
import qualified Data.Map.Strict as Map
import Data.Monoid (All (..), Any (..), Dual (..),
First (..), Last (..),
Product (..), Sum (..))
import Data.Semigroup (Semigroup (..))
import qualified Data.Semigroup as Semi
import Data.Tagged (Tagged (..))
import Data.Text (Text)
Expand Down
10 changes: 5 additions & 5 deletions test/Web/Internal/FormUrlEncodedSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Web.Internal.FormUrlEncodedSpec (spec) where

import Control.Monad ((<=<))
import qualified Data.ByteString.Lazy.Char8 as BSL
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Map.Strict as Map
import Data.Text (Text, unpack)
import Test.Hspec
import Test.QuickCheck
Expand All @@ -27,13 +27,13 @@ genericSpec = describe "Default (generic) instances" $ do

it "contains the record names" $ property $ \(x :: SimpleRec) -> do
let f = unForm $ toForm x
HashMap.member "rec1" f `shouldBe` True
HashMap.member "rec2" f `shouldBe` True
Map.member "rec1" f `shouldBe` True
Map.member "rec2" f `shouldBe` True

it "contains the correct record values" $ property $ \(x :: SimpleRec) -> do
let f = unForm $ toForm x
HashMap.lookup "rec1" f `shouldBe` Just [rec1 x]
(parseQueryParams <$> HashMap.lookup "rec2" f) `shouldBe` Just (Right [rec2 x])
Map.lookup "rec1" f `shouldBe` Just [rec1 x]
(parseQueryParams <$> Map.lookup "rec2" f) `shouldBe` Just (Right [rec2 x])

context "FromForm" $ do

Expand Down
6 changes: 3 additions & 3 deletions test/Web/Internal/TestInstances.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ module Web.Internal.TestInstances
, NoEmptyKeyForm(..)
) where

import Control.Applicative
import Control.Applicative -- for ghc < 9.6
import Data.Char
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Map.Strict as Map
import qualified Data.Text as T
import Data.Time.Compat
import GHC.Exts (fromList)
Expand Down Expand Up @@ -63,4 +63,4 @@ newtype NoEmptyKeyForm =
instance Arbitrary NoEmptyKeyForm where
arbitrary = NoEmptyKeyForm . removeEmptyKeys <$> arbitrary
where
removeEmptyKeys (Form m) = Form (HashMap.delete "" m)
removeEmptyKeys (Form m) = Form (Map.delete "" m)