https://haskell.github.io/security-advisories/advisory/HSEC-2026-0007.html
The year parser loops over digit characters with no upper bound constraint. The accumulated digits are then passed to textToInteger, which converts the arbitrarily long decimal string into a Haskell Integer (an arbitrary-precision bignum). Because this conversion is super-linear in the number of digits, an attacker can send a JSON date string with millions of digits in the year position (e.g., {"date": "999...999-01-01T00:00:00Z"}). A relatively small payload (~1MB) can cause seconds of CPU time and hundreds of megabytes of memory consumption, creating a practical asymmetric DoS vector.
But the same is true for number literals in JSON file.
Take files with repeated digit 1, and a program which reads it and e.g. computes a logarithm of it (something quick to do)
The million digit input takes 29MB of total memory in use and roughly 0.14 seconds mutator time on my machine (see below).
The literal parsing code uses byteStringToInteger (
|
int = byteStringToInteger (BS.Unsafe.unsafeTake n bs0) |
) which is essentially the same as
textToInteger
Why years are somehow special, though the superlinear (but sub quadratic!) behavior is very much inherent to aeson.
module Main (main) where
import System.Environment (getArgs)
import qualified Data.ByteString as BS
import Data.Aeson (throwDecodeStrict)
import Math.NumberTheory.Logarithms (integerLog10)
main :: IO ()
main = do
filename : _ <- getArgs
print filename
contents <- BS.readFile filename
n <- throwDecodeStrict contents
print $! integerLog10 n
% ls -lh
total 11M
-rw-rw-r-- 1 phadej phadej 237 Sep 14 21:36 aeson-literal.cabal
-rw-rw-r-- 1 phadej phadej 9.6M Sep 14 21:37 data-10000000.json
-rw-rw-r-- 1 phadej phadej 977K Sep 14 21:37 data-1000000.json
-rw-rw-r-- 1 phadej phadej 98K Sep 14 21:37 data-100000.json
-rw-rw-r-- 1 phadej phadej 9.8K Sep 14 21:37 data-10000.json
-rw-rw-r-- 1 phadej phadej 1000 Sep 14 21:37 data-1000.json
drwxrwxr-x 6 phadej phadej 4.0K Sep 14 21:30 dist-newstyle
-rw-rw-r-- 1 phadej phadej 359 Sep 14 21:37 generate.hs
-rw-rw-r-- 1 phadej phadej 365 Sep 14 21:35 run.hs
% cabal run run -- data-1000.json +RTS -s
"data-1000.json"
999
153,072 bytes allocated in the heap
3,832 bytes copied during GC
53,096 bytes maximum residency (1 sample(s))
28,824 bytes maximum slop
6 MiB total memory in use (0 MB lost due to fragmentation)
% cabal run run -- data-10000.json +RTS -s
"data-10000.json"
9999
783,320 bytes allocated in the heap
3,832 bytes copied during GC
53,096 bytes maximum residency (1 sample(s))
28,824 bytes maximum slop
6 MiB total memory in use (0 MB lost due to fragmentation)
% cabal run run -- data-100000.json +RTS -s
"data-100000.json"
99999
6,728,624 bytes allocated in the heap
150,040 bytes copied during GC
44,328 bytes maximum residency (1 sample(s))
29,400 bytes maximum slop
8 MiB total memory in use (0 MB lost due to fragmentation)
% cabal run run -- data-1000000.json +RTS -s
"data-1000000.json"
999999
71,560,416 bytes allocated in the heap
26,950,472 bytes copied during GC
9,201,496 bytes maximum residency (2 sample(s))
194,728 bytes maximum slop
29 MiB total memory in use (3 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 13 colls, 0 par 0.021s 0.021s 0.0016s 0.0042s
Gen 1 2 colls, 0 par 0.006s 0.007s 0.0033s 0.0049s
INIT time 0.000s ( 0.000s elapsed)
MUT time 0.137s ( 0.138s elapsed)
GC time 0.028s ( 0.028s elapsed)
EXIT time 0.000s ( 0.000s elapsed)
Total time 0.165s ( 0.166s elapsed)
% cabal run run -- data-10000000.json +RTS -s
"data-10000000.json"
9999999
771,606,872 bytes allocated in the heap
473,703,144 bytes copied during GC
107,164,456 bytes maximum residency (5 sample(s))
1,399,352 bytes maximum slop
228 MiB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 157 colls, 0 par 0.309s 0.311s 0.0020s 0.0134s
Gen 1 5 colls, 0 par 0.145s 0.145s 0.0290s 0.0674s
INIT time 0.000s ( 0.000s elapsed)
MUT time 2.005s ( 2.008s elapsed)
GC time 0.454s ( 0.456s elapsed)
EXIT time 0.000s ( 0.000s elapsed)
Total time 2.459s ( 2.465s elapsed)
https://haskell.github.io/security-advisories/advisory/HSEC-2026-0007.html
But the same is true for number literals in JSON file.
Take files with repeated digit
1, and a program which reads it and e.g. computes a logarithm of it (something quick to do)The million digit input takes 29MB of total memory in use and roughly 0.14 seconds mutator time on my machine (see below).
The literal parsing code uses
byteStringToInteger(aeson/src/Data/Aeson/Decoding/ByteString.hs
Line 214 in 682162c
textToIntegerWhy years are somehow special, though the superlinear (but sub quadratic!) behavior is very much inherent to
aeson.