Skip to content

Commit f7e380f

Browse files
authored
Merge pull request #645 from bos/bse
Add bounds on conversions from Scientific
2 parents 64715d4 + d354445 commit f7e380f

File tree

8 files changed

+105
-52
lines changed

8 files changed

+105
-52
lines changed

Data/Aeson.hs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ module Data.Aeson
113113
, withObject
114114
, withText
115115
, withArray
116-
, withNumber
117116
, withScientific
118117
, withBool
119118
, withEmbeddedJSON

Data/Aeson/Encoding/Builder.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ c2w c = fromIntegral (ord c)
133133
-- | Encode a JSON number.
134134
scientific :: Scientific -> Builder
135135
scientific s
136-
| e < 0 = scientificBuilder s
136+
| e < 0 || e > 1024 = scientificBuilder s
137137
| otherwise = B.integerDec (coefficient s * 10 ^ e)
138138
where
139139
e = base10Exponent s

Data/Aeson/Types.hs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ module Data.Aeson.Types
8181
, withObject
8282
, withText
8383
, withArray
84-
, withNumber
8584
, withScientific
8685
, withBool
8786
, withEmbeddedJSON

Data/Aeson/Types/Class.hs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ module Data.Aeson.Types.Class
7070
, withObject
7171
, withText
7272
, withArray
73-
, withNumber
7473
, withScientific
7574
, withBool
7675
, withEmbeddedJSON

Data/Aeson/Types/FromJSON.hs

Lines changed: 47 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ module Data.Aeson.Types.FromJSON
5252
, withObject
5353
, withText
5454
, withArray
55-
, withNumber
5655
, withScientific
5756
, withBool
5857
, withEmbeddedJSON
@@ -86,7 +85,6 @@ import Data.Aeson.Internal.Functions (mapKey)
8685
import Data.Aeson.Parser.Internal (eitherDecodeWith, jsonEOF)
8786
import Data.Aeson.Types.Generic
8887
import Data.Aeson.Types.Internal
89-
import Data.Attoparsec.Number (Number(..))
9088
import Data.Bits (unsafeShiftR)
9189
import Data.Fixed (Fixed, HasResolution)
9290
import Data.Functor.Compose (Compose(..))
@@ -100,7 +98,7 @@ import Data.Maybe (fromMaybe)
10098
import Data.Semigroup ((<>))
10199
import Data.Proxy (Proxy(..))
102100
import Data.Ratio ((%), Ratio)
103-
import Data.Scientific (Scientific)
101+
import Data.Scientific (Scientific, base10Exponent)
104102
import Data.Tagged (Tagged(..))
105103
import Data.Text (Text, pack, unpack)
106104
import Data.Time (Day, DiffTime, LocalTime, NominalDiffTime, TimeOfDay, UTCTime, ZonedTime)
@@ -173,15 +171,6 @@ parseIndexedJSONPair keyParser valParser idx value = p value <?> Index idx
173171
parseJSONElemAtIndex :: (Value -> Parser a) -> Int -> V.Vector Value -> Parser a
174172
parseJSONElemAtIndex p idx ary = p (V.unsafeIndex ary idx) <?> Index idx
175173

176-
scientificToNumber :: Scientific -> Number
177-
scientificToNumber s
178-
| e < 0 = D $ Scientific.toRealFloat s
179-
| otherwise = I $ c * 10 ^ e
180-
where
181-
e = Scientific.base10Exponent s
182-
c = Scientific.coefficient s
183-
{-# INLINE scientificToNumber #-}
184-
185174
parseRealFloat :: RealFloat a => String -> Value -> Parser a
186175
parseRealFloat _ (Number s) = pure $ Scientific.toRealFloat s
187176
parseRealFloat _ Null = pure (0/0)
@@ -197,7 +186,7 @@ parseIntegralFromScientific expected s =
197186

198187
parseIntegral :: Integral a => String -> Value -> Parser a
199188
parseIntegral expected =
200-
withScientific expected $ parseIntegralFromScientific expected
189+
withBoundedScientific expected $ parseIntegralFromScientific expected
201190
{-# INLINE parseIntegral #-}
202191

203192
parseBoundedIntegralFromScientific :: (Bounded a, Integral a) => String -> Scientific -> Parser a
@@ -219,8 +208,13 @@ parseScientificText
219208
. T.encodeUtf8
220209

221210
parseIntegralText :: Integral a => String -> Text -> Parser a
222-
parseIntegralText expected t =
223-
parseScientificText t >>= parseIntegralFromScientific expected
211+
parseIntegralText expected t
212+
= parseScientificText t
213+
>>= rejectLargeExponent
214+
>>= parseIntegralFromScientific expected
215+
where
216+
rejectLargeExponent :: Scientific -> Parser Scientific
217+
rejectLargeExponent s = withBoundedScientific expected pure (Number s)
224218
{-# INLINE parseIntegralText #-}
225219

226220
parseBoundedIntegralText :: (Bounded a, Integral a) => String -> Text -> Parser a
@@ -646,21 +640,33 @@ withArray _ f (Array arr) = f arr
646640
withArray expected _ v = typeMismatch expected v
647641
{-# INLINE withArray #-}
648642

649-
-- | @'withNumber' expected f value@ applies @f@ to the 'Number' when @value@
650-
-- is a 'Number' and fails using @'typeMismatch' expected@ otherwise.
651-
withNumber :: String -> (Number -> Parser a) -> Value -> Parser a
652-
withNumber expected f = withScientific expected (f . scientificToNumber)
653-
{-# INLINE withNumber #-}
654-
{-# DEPRECATED withNumber "Use withScientific instead" #-}
655-
656643
-- | @'withScientific' expected f value@ applies @f@ to the 'Scientific' number
657644
-- when @value@ is a 'Number' and fails using @'typeMismatch' expected@
658645
-- otherwise.
646+
-- .
647+
-- /Warning/: If you are converting from a scientific to an unbounded
648+
-- type such as 'Integer' you may want to add a restriction on the
649+
-- size of the exponent (see 'withBoundedScientific') to prevent
650+
-- malicious input from filling up the memory of the target system.
659651
withScientific :: String -> (Scientific -> Parser a) -> Value -> Parser a
660652
withScientific _ f (Number scientific) = f scientific
661653
withScientific expected _ v = typeMismatch expected v
662654
{-# INLINE withScientific #-}
663655

656+
-- | @'withBoundedScientific' expected f value@ applies @f@ to the 'Scientific' number
657+
-- when @value@ is a 'Number' and fails using @'typeMismatch' expected@
658+
-- otherwise.
659+
--
660+
-- The conversion will also fail wyth a @'typeMismatch' if the
661+
-- 'Scientific' exponent is larger than 1024.
662+
withBoundedScientific :: String -> (Scientific -> Parser a) -> Value -> Parser a
663+
withBoundedScientific _ f v@(Number scientific) =
664+
if base10Exponent scientific > 1024
665+
then typeMismatch "a number with exponent <= 1024" v
666+
else f scientific
667+
withBoundedScientific expected _ v = typeMismatch expected v
668+
{-# INLINE withBoundedScientific #-}
669+
664670
-- | @'withBool' expected f value@ applies @f@ to the 'Bool' when @value@ is a
665671
-- 'Bool' and fails using @'typeMismatch' expected@ otherwise.
666672
withBool :: String -> (Bool -> Parser a) -> Value -> Parser a
@@ -1240,12 +1246,6 @@ instance FromJSONKey Double where
12401246
"-Infinity" -> pure (negate 1/0)
12411247
_ -> Scientific.toRealFloat <$> parseScientificText t
12421248

1243-
instance FromJSON Number where
1244-
parseJSON (Number s) = pure $ scientificToNumber s
1245-
parseJSON Null = pure (D (0/0))
1246-
parseJSON v = typeMismatch "Number" v
1247-
{-# INLINE parseJSON #-}
1248-
12491249
instance FromJSON Float where
12501250
parseJSON = parseRealFloat "Float"
12511251
{-# INLINE parseJSON #-}
@@ -1266,12 +1266,12 @@ instance (FromJSON a, Integral a) => FromJSON (Ratio a) where
12661266
else pure $ numerator % denominator
12671267
{-# INLINE parseJSON #-}
12681268

1269-
-- | /WARNING:/ Only parse fixed-precision numbers from trusted input
1270-
-- since an attacker could easily fill up the memory of the target
1271-
-- system by specifying a scientific number with a big exponent like
1272-
-- @1e1000000000@.
1269+
-- | This instance includes a bounds check to prevent maliciously
1270+
-- large inputs to fill up the memory of the target system. You can
1271+
-- newtype 'Scientific' and provide your own instance using
1272+
-- 'withScientific' if you want to allow larger inputs.
12731273
instance HasResolution a => FromJSON (Fixed a) where
1274-
parseJSON = withScientific "Fixed" $ pure . realToFrac
1274+
parseJSON = withBoundedScientific "Fixed" $ pure . realToFrac
12751275
{-# INLINE parseJSON #-}
12761276

12771277
instance FromJSON Int where
@@ -1281,10 +1281,10 @@ instance FromJSON Int where
12811281
instance FromJSONKey Int where
12821282
fromJSONKey = FromJSONKeyTextParser $ parseBoundedIntegralText "Int"
12831283

1284-
-- | /WARNING:/ Only parse Integers from trusted input since an
1285-
-- attacker could easily fill up the memory of the target system by
1286-
-- specifying a scientific number with a big exponent like
1287-
-- @1e1000000000@.
1284+
-- | This instance includes a bounds check to prevent maliciously
1285+
-- large inputs to fill up the memory of the target system. You can
1286+
-- newtype 'Scientific' and provide your own instance using
1287+
-- 'withScientific' if you want to allow larger inputs.
12881288
instance FromJSON Integer where
12891289
parseJSON = parseIntegral "Integer"
12901290
{-# INLINE parseJSON #-}
@@ -1723,21 +1723,21 @@ instance FromJSONKey UTCTime where
17231723
fromJSONKey = FromJSONKeyTextParser (Time.run Time.utcTime)
17241724

17251725

1726-
-- | /WARNING:/ Only parse lengths of time from trusted input
1727-
-- since an attacker could easily fill up the memory of the target
1728-
-- system by specifying a scientific number with a big exponent like
1729-
-- @1e1000000000@.
1726+
-- | This instance includes a bounds check to prevent maliciously
1727+
-- large inputs to fill up the memory of the target system. You can
1728+
-- newtype 'Scientific' and provide your own instance using
1729+
-- 'withScientific' if you want to allow larger inputs.
17301730
instance FromJSON NominalDiffTime where
1731-
parseJSON = withScientific "NominalDiffTime" $ pure . realToFrac
1731+
parseJSON = withBoundedScientific "NominalDiffTime" $ pure . realToFrac
17321732
{-# INLINE parseJSON #-}
17331733

17341734

1735-
-- | /WARNING:/ Only parse lengths of time from trusted input
1736-
-- since an attacker could easily fill up the memory of the target
1737-
-- system by specifying a scientific number with a big exponent like
1738-
-- @1e1000000000@.
1735+
-- | This instance includes a bounds check to prevent maliciously
1736+
-- large inputs to fill up the memory of the target system. You can
1737+
-- newtype 'Scientific' and provide your own instance using
1738+
-- 'withScientific' if you want to allow larger inputs.
17391739
instance FromJSON DiffTime where
1740-
parseJSON = withScientific "DiffTime" $ pure . realToFrac
1740+
parseJSON = withBoundedScientific "DiffTime" $ pure . realToFrac
17411741
{-# INLINE parseJSON #-}
17421742

17431743
-------------------------------------------------------------------------------

aeson.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: aeson
2-
version: 1.3.1.1
2+
version: 1.4.0.0
33
license: BSD3
44
license-file: LICENSE
55
category: Text, Web, JSON

changelog.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
For the latest version of this document, please see [https://github.com/bos/aeson/blob/master/changelog.md](https://github.com/bos/aeson/blob/master/changelog.md).
22

3+
### 1.4.0.0
4+
5+
This release introduces bounds on the size of `Scientific` numbers when they are converted to other arbitrary precision types that do not represent them efficiently in memory.
6+
7+
This means that trying to decode a number such as `1e1000000000` into an `Integer` will now fail instead of using a lot of memory. If you need to represent large numbers you can add a newtype (preferably over `Scientific`) and providing a parser using `withScientific`.
8+
9+
The following instances are affected by this:
10+
* `FromJSON Natural`
11+
* `FromJSONKey Natural`
12+
* `FromJSON Integer`
13+
* `FromJSONKey Integer`
14+
* `FromJSON NominalDiffTime`
15+
16+
For the same reasons the following instances & functions have been removed:
17+
* Remove `FromJSON Data.Attoparsec.Number` instance. Note that `Data.Attoparsec.Number` is deprecated.
18+
* Remove deprecated `withNumber`, use `withScientific` instead.
19+
20+
Finally, encoding integral values with large exponents now uses scientific notation, this saves space for large numbers.
21+
322
### 1.3.1.1
423

524
* Catch 0 denominators when parsing Ratio

tests/UnitTests.hs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,19 @@ import Data.Aeson.Types (Options(..), Result(Success), ToJSON(..), Value(Null),
3131
import Data.Char (toUpper)
3232
import Data.Either.Compat (isLeft, isRight)
3333
import Data.Hashable (hash)
34+
import Data.HashMap.Strict (HashMap)
3435
import Data.List (sort)
3536
import Data.Maybe (fromMaybe)
3637
import Data.Sequence (Seq)
38+
import Data.Scientific (Scientific, scientific)
3739
import Data.Tagged (Tagged(..))
3840
import Data.Text (Text)
3941
import Data.Time (UTCTime)
4042
import Data.Time.Format (parseTime)
4143
import Data.Time.Locale.Compat (defaultTimeLocale)
4244
import GHC.Generics (Generic)
4345
import Instances ()
46+
import Numeric.Natural (Natural)
4447
import System.Directory (getDirectoryContents)
4548
import System.FilePath ((</>), takeExtension, takeFileName)
4649
import Test.Tasty (TestTree, testGroup)
@@ -100,6 +103,10 @@ tests = testGroup "unit" [
100103
, testCase "withEmbeddedJSON" withEmbeddedJSONTest
101104
, testCase "SingleFieldCon" singleFieldCon
102105
, testCase "Ratio with denominator 0" ratioDenominator0
106+
, testCase "Big scientific exponent" bigScientificExponent
107+
, testCase "Big integer decoding" bigIntegerDecoding
108+
, testCase "Big natural decading" bigNaturalDecoding
109+
, testCase "Big integer key decoding" bigIntegerKeyDecoding
103110
]
104111

105112
roundTripCamel :: String -> Assertion
@@ -552,6 +559,36 @@ ratioDenominator0 =
552559
(Left "Error in $: Ratio denominator was 0")
553560
(eitherDecode "{ \"numerator\": 1, \"denominator\": 0 }" :: Either String Rational)
554561

562+
bigScientificExponent :: Assertion
563+
bigScientificExponent =
564+
assertEqual "Encoding an integral scientific with a large exponent should normalize it"
565+
"1.0e2000"
566+
(encode (scientific 1 2000 :: Scientific))
567+
568+
bigIntegerDecoding :: Assertion
569+
bigIntegerDecoding =
570+
assertEqual "Decoding an Integer with a large exponent should fail"
571+
(Left "Error in $: expected a number with exponent <= 1024, encountered Number")
572+
((eitherDecode :: L.ByteString -> Either String Integer) "1e2000")
573+
574+
bigNaturalDecoding :: Assertion
575+
bigNaturalDecoding =
576+
assertEqual "Decoding a Natural with a large exponent should fail"
577+
(Left "Error in $: expected a number with exponent <= 1024, encountered Number")
578+
((eitherDecode :: L.ByteString -> Either String Integer) "1e2000")
579+
580+
bigIntegerKeyDecoding :: Assertion
581+
bigIntegerKeyDecoding =
582+
assertEqual "Decoding an Integer key with a large exponent should fail"
583+
(Left "Error in $['1e2000']: expected a number with exponent <= 1024, encountered Number")
584+
((eitherDecode :: L.ByteString -> Either String (HashMap Integer Value)) "{ \"1e2000\": null }")
585+
586+
bigNaturalKeyDecoding :: Assertion
587+
bigNaturalKeyDecoding =
588+
assertEqual "Decoding an Integer key with a large exponent should fail"
589+
(Left "Error in $['1e2000']: expected a number with exponent <= 1024, encountered Number")
590+
((eitherDecode :: L.ByteString -> Either String (HashMap Natural Value)) "{ \"1e2000\": null }")
591+
555592
deriveJSON defaultOptions{omitNothingFields=True} ''MyRecord
556593

557594
deriveToJSON defaultOptions ''Foo

0 commit comments

Comments
 (0)