{-# LANGUAGE OverloadedStrings #-}
module Convex.ThreatModel.DatumBloat (
datumListBloatAttack,
datumListBloatAttackWith,
datumListBloatAttackWithGen,
bloatLists,
datumByteBloatAttack,
datumByteBloatAttackWith,
datumByteBloatAttackWithGen,
inflateBytes,
inflateFirstListItem,
) where
import Convex.ThreatModel
import Data.ByteString qualified as BS
import Test.QuickCheck (Gen, choose)
datumListBloatAttack :: ThreatModel ()
datumListBloatAttack :: ThreatModel ()
datumListBloatAttack = Gen (Int, Int) -> ThreatModel ()
datumListBloatAttackWithGen ((,) (Int -> Int -> (Int, Int)) -> Gen Int -> Gen (Int -> (Int, Int))
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> (Int, Int) -> Gen Int
forall a. Random a => (a, a) -> Gen a
choose (Int
1, Int
20) Gen (Int -> (Int, Int)) -> Gen Int -> Gen (Int, Int)
forall a b. Gen (a -> b) -> Gen a -> Gen b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> (Int, Int) -> Gen Int
forall a. Random a => (a, a) -> Gen a
choose (Int
1, Int
500))
datumListBloatAttackWith :: Int -> Int -> ThreatModel ()
datumListBloatAttackWith :: Int -> Int -> ThreatModel ()
datumListBloatAttackWith Int
numItems Int
itemSize = Gen (Int, Int) -> ThreatModel ()
datumListBloatAttackWithGen ((Int, Int) -> Gen (Int, Int)
forall a. a -> Gen a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Int
numItems, Int
itemSize))
datumListBloatAttackWithGen :: Gen (Int, Int) -> ThreatModel ()
datumListBloatAttackWithGen :: Gen (Int, Int) -> ThreatModel ()
datumListBloatAttackWithGen Gen (Int, Int)
gen =
String -> ThreatModel () -> ThreatModel ()
forall a. String -> ThreatModel a -> ThreatModel a
Named String
"Datum List Bloat Attack" (ThreatModel () -> ThreatModel ())
-> ThreatModel () -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$ do
(Int
numItems, Int
itemSize) <- Gen (Int, Int)
-> ((Int, Int) -> [(Int, Int)]) -> ThreatModel (Int, Int)
forall a. Show a => Gen a -> (a -> [a]) -> ThreatModel a
forAllTM Gen (Int, Int)
gen (Int, Int) -> [(Int, Int)]
shrinkPair
Bool -> ThreatModel ()
ensure (Int
numItems Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
1 Bool -> Bool -> Bool
&& Int
itemSize Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
1)
(Output
target, ScriptData
originalDatum) <- ThreatModel (Output, ScriptData)
anyGuardedOutputWithInlineDatum
Bool -> ThreatModel () -> ThreatModel ()
forall {f :: * -> *}. Applicative f => Bool -> f () -> f ()
unless (ScriptData -> Bool
containsList ScriptData
originalDatum) (ThreatModel () -> ThreatModel ())
-> ThreatModel () -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
String -> ThreatModel ()
forall a. String -> ThreatModel a
failPrecondition String
"Datum contains no list fields to bloat"
let bloatedDatum :: ScriptData
bloatedDatum = Int -> Int -> ScriptData -> ScriptData
bloatLists Int
numItems Int
itemSize ScriptData
originalDatum
String -> ThreatModel ()
counterexampleTM (String -> ThreatModel ()) -> String -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
[String] -> String
paragraph
[ String
"The transaction contains a script output at index"
, TxIx -> String
forall a. Show a => a -> String
show (Output -> TxIx
outputIx Output
target)
, String
"with an inline datum containing list fields."
]
String -> ThreatModel ()
counterexampleTM (String -> ThreatModel ()) -> String -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
[String] -> String
paragraph
[ String
"Testing if the lists can be bloated with"
, Int -> String
forall a. Show a => a -> String
show Int
numItems
, String
"items of"
, Int -> String
forall a. Show a => a -> String
show Int
itemSize
, String
"bytes each while still passing validation."
]
String -> ThreatModel ()
counterexampleTM (String -> ThreatModel ()) -> String -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
[String] -> String
paragraph
[ String
"If this validates, the script doesn't enforce datum field size limits."
, String
"An attacker could exploit this to:"
, String
"1) Inflate the datum beyond transaction size limits"
, String
"2) Increase execution costs for processing the datum"
, String
"3) Potentially lock funds permanently if limits are exceeded"
]
String -> [String] -> ThreatModel ()
tabulateTM String
"items" [Int -> String
bucketItems Int
numItems]
String -> [String] -> ThreatModel ()
tabulateTM String
"item bytes" [Int -> String
bucketSize Int
itemSize]
TxModifier -> ThreatModel ()
shouldNotValidate (TxModifier -> ThreatModel ()) -> TxModifier -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$ Output -> Datum -> TxModifier
forall t. IsInputOrOutput t => t -> Datum -> TxModifier
changeDatumOf Output
target (ScriptData -> Datum
toInlineDatum ScriptData
bloatedDatum)
where
unless :: Bool -> f () -> f ()
unless Bool
False f ()
action = f ()
action
unless Bool
True f ()
_ = () -> f ()
forall a. a -> f a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ()
shrinkPair :: (Int, Int) -> [(Int, Int)]
shrinkPair :: (Int, Int) -> [(Int, Int)]
shrinkPair (Int
a, Int
b) =
[(Int
a', Int
b) | Int
a' <- Int -> [Int]
shrinkPositive Int
a]
[(Int, Int)] -> [(Int, Int)] -> [(Int, Int)]
forall a. [a] -> [a] -> [a]
++ [(Int
a, Int
b') | Int
b' <- Int -> [Int]
shrinkPositive Int
b]
bucketItems :: Int -> String
bucketItems :: Int -> String
bucketItems Int
n
| Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
5 = String
"001-005"
| Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
10 = String
"006-010"
| Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
15 = String
"011-015"
| Bool
otherwise = String
"016-020"
bucketSize :: Int -> String
bucketSize :: Int -> String
bucketSize Int
n
| Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
50 = String
"001-050"
| Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
100 = String
"051-100"
| Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
250 = String
"101-250"
| Bool
otherwise = String
"251-500"
bloatLists :: Int -> Int -> ScriptData -> ScriptData
bloatLists :: Int -> Int -> ScriptData -> ScriptData
bloatLists Int
numItems Int
itemSize = ScriptData -> ScriptData
go
where
largeItem :: ScriptData
largeItem = ByteString -> ScriptData
ScriptDataBytes (Int -> Word8 -> ByteString
BS.replicate Int
itemSize Word8
0x42)
go :: ScriptData -> ScriptData
go (ScriptDataConstructor Integer
idx [ScriptData]
fields) =
Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx ((ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
go [ScriptData]
fields)
go (ScriptDataList [ScriptData]
items) =
[ScriptData] -> ScriptData
ScriptDataList ((ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
go [ScriptData]
items [ScriptData] -> [ScriptData] -> [ScriptData]
forall a. [a] -> [a] -> [a]
++ Int -> ScriptData -> [ScriptData]
forall a. Int -> a -> [a]
replicate Int
numItems ScriptData
largeItem)
go (ScriptDataMap [(ScriptData, ScriptData)]
entries) =
[(ScriptData, ScriptData)] -> ScriptData
ScriptDataMap [(ScriptData -> ScriptData
go ScriptData
k, ScriptData -> ScriptData
go ScriptData
v) | (ScriptData
k, ScriptData
v) <- [(ScriptData, ScriptData)]
entries]
go ScriptData
other = ScriptData
other
containsList :: ScriptData -> Bool
containsList :: ScriptData -> Bool
containsList (ScriptDataConstructor Integer
_ [ScriptData]
fields) = (ScriptData -> Bool) -> [ScriptData] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any ScriptData -> Bool
containsList [ScriptData]
fields
containsList (ScriptDataList [ScriptData]
_) = Bool
True
containsList (ScriptDataMap [(ScriptData, ScriptData)]
entries) = ((ScriptData, ScriptData) -> Bool)
-> [(ScriptData, ScriptData)] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (\(ScriptData
k, ScriptData
v) -> ScriptData -> Bool
containsList ScriptData
k Bool -> Bool -> Bool
|| ScriptData -> Bool
containsList ScriptData
v) [(ScriptData, ScriptData)]
entries
containsList ScriptData
_ = Bool
False
datumByteBloatAttack :: ThreatModel ()
datumByteBloatAttack :: ThreatModel ()
datumByteBloatAttack = Gen Int -> ThreatModel ()
datumByteBloatAttackWithGen ((Int, Int) -> Gen Int
forall a. Random a => (a, a) -> Gen a
choose (Int
1, Int
10000))
datumByteBloatAttackWith :: Int -> ThreatModel ()
datumByteBloatAttackWith :: Int -> ThreatModel ()
datumByteBloatAttackWith = Gen Int -> ThreatModel ()
datumByteBloatAttackWithGen (Gen Int -> ThreatModel ())
-> (Int -> Gen Int) -> Int -> ThreatModel ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Gen Int
forall a. a -> Gen a
forall (f :: * -> *) a. Applicative f => a -> f a
pure
datumByteBloatAttackWithGen :: Gen Int -> ThreatModel ()
datumByteBloatAttackWithGen :: Gen Int -> ThreatModel ()
datumByteBloatAttackWithGen Gen Int
gen =
String -> ThreatModel () -> ThreatModel ()
forall a. String -> ThreatModel a -> ThreatModel a
Named String
"Datum Byte Bloat Attack" (ThreatModel () -> ThreatModel ())
-> ThreatModel () -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$ do
Int
inflatedSize <- Gen Int -> (Int -> [Int]) -> ThreatModel Int
forall a. Show a => Gen a -> (a -> [a]) -> ThreatModel a
forAllTM Gen Int
gen Int -> [Int]
shrinkPositive
Bool -> ThreatModel ()
ensure (Int
inflatedSize Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
1)
(Output
target, ScriptData
originalDatum) <- ThreatModel (Output, ScriptData)
anyGuardedOutputWithInlineDatum
let bloatedDatum :: ScriptData
bloatedDatum = Int -> ScriptData -> ScriptData
inflateFirstListItem Int
inflatedSize ScriptData
originalDatum
ThreatModel () -> ThreatModel ()
forall a. ThreatModel a -> ThreatModel a
threatPrecondition (ThreatModel () -> ThreatModel ())
-> ThreatModel () -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$ Bool -> ThreatModel ()
ensure (ScriptData
bloatedDatum ScriptData -> ScriptData -> Bool
forall a. Eq a => a -> a -> Bool
/= ScriptData
originalDatum)
String -> ThreatModel ()
counterexampleTM (String -> ThreatModel ()) -> String -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
[String] -> String
paragraph
[ String
"The transaction contains a script output with an inline datum."
, String
"Testing if the first item in list fields can be inflated to"
, Int -> String
forall a. Show a => a -> String
show Int
inflatedSize
, String
"bytes while still passing validation."
]
String -> ThreatModel ()
counterexampleTM (String -> ThreatModel ()) -> String -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
[String] -> String
paragraph
[ String
"If this validates, the script doesn't limit ByteString field sizes,"
, String
"enabling a datum bloat DoS attack where an attacker can add"
, String
"a huge message/data item to bloat the datum beyond spendable limits."
]
String -> [String] -> ThreatModel ()
tabulateTM String
"inflated bytes" [Int -> String
bucket Int
inflatedSize]
TxModifier -> ThreatModel ()
shouldNotValidate (TxModifier -> ThreatModel ()) -> TxModifier -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$ Output -> Datum -> TxModifier
forall t. IsInputOrOutput t => t -> Datum -> TxModifier
changeDatumOf Output
target (ScriptData -> Datum
toInlineDatum ScriptData
bloatedDatum)
bucket :: Int -> String
bucket :: Int -> String
bucket Int
n
| Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
1000 = String
"0001-1000"
| Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
5000 = String
"1001-5000"
| Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
10000 = String
"5001-10000"
| Bool
otherwise = String
"10000+"
inflateBytes :: Int -> ScriptData -> ScriptData
inflateBytes :: Int -> ScriptData -> ScriptData
inflateBytes Int
size = ScriptData -> ScriptData
goTop
where
largeBytes :: ByteString
largeBytes = Int -> Word8 -> ByteString
BS.replicate Int
size Word8
0x42
goTop :: ScriptData -> ScriptData
goTop (ScriptDataConstructor Integer
idx [ScriptData]
fields) =
case [ScriptData]
fields of
(ScriptData
first : [ScriptData]
rest) -> Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx (ScriptData
first ScriptData -> [ScriptData] -> [ScriptData]
forall a. a -> [a] -> [a]
: (ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
go [ScriptData]
rest)
[] -> Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx []
goTop ScriptData
other = ScriptData -> ScriptData
go ScriptData
other
go :: ScriptData -> ScriptData
go (ScriptDataConstructor Integer
idx [ScriptData]
fields) = Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx ((ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
go [ScriptData]
fields)
go (ScriptDataList [ScriptData]
items) = [ScriptData] -> ScriptData
ScriptDataList ((ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
go [ScriptData]
items)
go (ScriptDataMap [(ScriptData, ScriptData)]
entries) = [(ScriptData, ScriptData)] -> ScriptData
ScriptDataMap [(ScriptData -> ScriptData
go ScriptData
k, ScriptData -> ScriptData
go ScriptData
v) | (ScriptData
k, ScriptData
v) <- [(ScriptData, ScriptData)]
entries]
go (ScriptDataBytes ByteString
_) = ByteString -> ScriptData
ScriptDataBytes ByteString
largeBytes
go ScriptData
other = ScriptData
other
inflateFirstListItem :: Int -> ScriptData -> ScriptData
inflateFirstListItem :: Int -> ScriptData -> ScriptData
inflateFirstListItem Int
size = ScriptData -> ScriptData
goTop
where
largeBytes :: ByteString
largeBytes = Int -> Word8 -> ByteString
BS.replicate Int
size Word8
0x42
goTop :: ScriptData -> ScriptData
goTop (ScriptDataConstructor Integer
idx [ScriptData]
fields) =
case [ScriptData]
fields of
(ScriptData
first : [ScriptData]
rest) -> Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx (ScriptData
first ScriptData -> [ScriptData] -> [ScriptData]
forall a. a -> [a] -> [a]
: (ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
goList [ScriptData]
rest)
[] -> Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx []
goTop ScriptData
other = ScriptData -> ScriptData
goList ScriptData
other
goList :: ScriptData -> ScriptData
goList (ScriptDataConstructor Integer
idx [ScriptData]
fields) = Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx ((ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
goList [ScriptData]
fields)
goList (ScriptDataList (ScriptData
firstItem : [ScriptData]
restItems)) =
[ScriptData] -> ScriptData
ScriptDataList (ScriptData -> ScriptData
inflateItem ScriptData
firstItem ScriptData -> [ScriptData] -> [ScriptData]
forall a. a -> [a] -> [a]
: [ScriptData]
restItems)
goList (ScriptDataList []) = [ScriptData] -> ScriptData
ScriptDataList []
goList (ScriptDataMap [(ScriptData, ScriptData)]
entries) = [(ScriptData, ScriptData)] -> ScriptData
ScriptDataMap [(ScriptData -> ScriptData
goList ScriptData
k, ScriptData -> ScriptData
goList ScriptData
v) | (ScriptData
k, ScriptData
v) <- [(ScriptData, ScriptData)]
entries]
goList ScriptData
other = ScriptData
other
inflateItem :: ScriptData -> ScriptData
inflateItem (ScriptDataBytes ByteString
_) = ByteString -> ScriptData
ScriptDataBytes ByteString
largeBytes
inflateItem (ScriptDataConstructor Integer
idx [ScriptData]
fields) =
Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx ((ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
inflateItem [ScriptData]
fields)
inflateItem (ScriptDataList [ScriptData]
items) = [ScriptData] -> ScriptData
ScriptDataList ((ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
inflateItem [ScriptData]
items)
inflateItem (ScriptDataMap [(ScriptData, ScriptData)]
entries) =
[(ScriptData, ScriptData)] -> ScriptData
ScriptDataMap [(ScriptData -> ScriptData
inflateItem ScriptData
k, ScriptData -> ScriptData
inflateItem ScriptData
v) | (ScriptData
k, ScriptData
v) <- [(ScriptData, ScriptData)]
entries]
inflateItem ScriptData
other = ScriptData
other