Skip to content

Why the parseYear is considered security vulnerability? #1195

Description

@phadej

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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions