{-# LANGUAGE OverloadedStrings #-}
module Convex.ThreatModel.DatumBloat (
datumListBloatAttack,
datumListBloatAttackWith,
bloatLists,
datumByteBloatAttack,
datumByteBloatAttackWith,
inflateBytes,
inflateFirstListItem,
) where
import Convex.ThreatModel
import Data.ByteString qualified as BS
datumListBloatAttack :: ThreatModel ()
datumListBloatAttack :: ThreatModel ()
datumListBloatAttack = Int -> Int -> ThreatModel ()
datumListBloatAttackWith Int
5 Int
100
datumListBloatAttackWith :: Int -> Int -> ThreatModel ()
datumListBloatAttackWith :: Int -> Int -> ThreatModel ()
datumListBloatAttackWith Int
numItems Int
itemSize = [Char] -> ThreatModel () -> ThreatModel ()
forall a. [Char] -> ThreatModel a -> ThreatModel a
Named ([Char]
"Datum List Bloat Attack (" [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ Int -> [Char]
forall a. Show a => a -> [Char]
show Int
numItems [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ [Char]
" items, " [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ Int -> [Char]
forall a. Show a => a -> [Char]
show Int
itemSize [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ [Char]
" bytes)") (ThreatModel () -> ThreatModel ())
-> ThreatModel () -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$ do
[Output]
outputs <- ThreatModel [Output]
getTxOutputs
let scriptOutputsWithDatum :: [Output]
scriptOutputsWithDatum = (Output -> Bool) -> [Output] -> [Output]
forall a. (a -> Bool) -> [a] -> [a]
filter Output -> Bool
isScriptOutputWithInlineDatum [Output]
outputs
ThreatModel () -> ThreatModel ()
forall a. ThreatModel a -> ThreatModel a
threatPrecondition (ThreatModel () -> ThreatModel ())
-> ThreatModel () -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$ Bool -> ThreatModel ()
ensure (Bool -> Bool
not (Bool -> Bool) -> Bool -> Bool
forall a b. (a -> b) -> a -> b
$ [Output] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Output]
scriptOutputsWithDatum)
Output
target <- [Output] -> ThreatModel Output
forall a. Show a => [a] -> ThreatModel a
pickAny [Output]
scriptOutputsWithDatum
ScriptData
originalDatum <- case Output -> Maybe ScriptData
getInlineDatum Output
target of
Maybe ScriptData
Nothing -> [Char] -> ThreatModel ScriptData
forall a. [Char] -> ThreatModel a
failPrecondition [Char]
"Script output missing inline datum"
Just ScriptData
originalDatum' -> ScriptData -> ThreatModel ScriptData
forall a. a -> ThreatModel a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ScriptData
originalDatum'
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
$
[Char] -> ThreatModel ()
forall a. [Char] -> ThreatModel a
failPrecondition [Char]
"Datum contains no list fields to bloat"
let bloatedDatum :: ScriptData
bloatedDatum = Int -> Int -> ScriptData -> ScriptData
bloatLists Int
numItems Int
itemSize ScriptData
originalDatum
[Char] -> ThreatModel ()
counterexampleTM ([Char] -> ThreatModel ()) -> [Char] -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
[[Char]] -> [Char]
paragraph
[ [Char]
"The transaction contains a script output at index"
, TxIx -> [Char]
forall a. Show a => a -> [Char]
show (Output -> TxIx
outputIx Output
target)
, [Char]
"with an inline datum containing list fields."
]
[Char] -> ThreatModel ()
counterexampleTM ([Char] -> ThreatModel ()) -> [Char] -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
[[Char]] -> [Char]
paragraph
[ [Char]
"Testing if the lists can be bloated with"
, Int -> [Char]
forall a. Show a => a -> [Char]
show Int
numItems
, [Char]
"items of"
, Int -> [Char]
forall a. Show a => a -> [Char]
show Int
itemSize
, [Char]
"bytes each while still passing validation."
]
[Char] -> ThreatModel ()
counterexampleTM ([Char] -> ThreatModel ()) -> [Char] -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
[[Char]] -> [Char]
paragraph
[ [Char]
"If this validates, the script doesn't enforce datum field size limits."
, [Char]
"An attacker could exploit this to:"
, [Char]
"1) Inflate the datum beyond transaction size limits"
, [Char]
"2) Increase execution costs for processing the datum"
, [Char]
"3) Potentially lock funds permanently if limits are exceeded"
]
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 ()
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
isScriptOutputWithInlineDatum :: Output -> Bool
isScriptOutputWithInlineDatum :: Output -> Bool
isScriptOutputWithInlineDatum Output
output =
Bool -> Bool
not (AddressAny -> Bool
isKeyAddressAny (Output -> AddressAny
forall t. IsInputOrOutput t => t -> AddressAny
addressOf Output
output)) Bool -> Bool -> Bool
&& Output -> Bool
hasInlineDatum Output
output
hasInlineDatum :: Output -> Bool
hasInlineDatum :: Output -> Bool
hasInlineDatum Output
output =
case TxOut CtxTx Era -> Datum
forall ctx. TxOut ctx Era -> TxOutDatum ctx Era
datumOfTxOut (Output -> TxOut CtxTx Era
outputTxOut Output
output) of
TxOutDatumInline{} -> Bool
True
Datum
_ -> Bool
False
getInlineDatum :: Output -> Maybe ScriptData
getInlineDatum :: Output -> Maybe ScriptData
getInlineDatum Output
output =
case TxOut CtxTx Era -> Datum
forall ctx. TxOut ctx Era -> TxOutDatum ctx Era
datumOfTxOut (Output -> TxOut CtxTx Era
outputTxOut Output
output) of
TxOutDatumInline BabbageEraOnwards Era
_ HashableScriptData
hashableData -> ScriptData -> Maybe ScriptData
forall a. a -> Maybe a
Just (HashableScriptData -> ScriptData
getScriptData HashableScriptData
hashableData)
Datum
_ -> Maybe ScriptData
forall a. Maybe a
Nothing
toInlineDatum :: ScriptData -> Datum
toInlineDatum :: ScriptData -> Datum
toInlineDatum ScriptData
sd =
BabbageEraOnwards Era -> HashableScriptData -> Datum
forall era ctx.
BabbageEraOnwards era -> HashableScriptData -> TxOutDatum ctx era
TxOutDatumInline BabbageEraOnwards Era
BabbageEraOnwardsConway (ScriptData -> HashableScriptData
unsafeHashableScriptData ScriptData
sd)
datumByteBloatAttack :: ThreatModel ()
datumByteBloatAttack :: ThreatModel ()
datumByteBloatAttack = Int -> ThreatModel ()
datumByteBloatAttackWith Int
10000
datumByteBloatAttackWith :: Int -> ThreatModel ()
datumByteBloatAttackWith :: Int -> ThreatModel ()
datumByteBloatAttackWith Int
inflatedSize = [Char] -> ThreatModel () -> ThreatModel ()
forall a. [Char] -> ThreatModel a -> ThreatModel a
Named ([Char]
"Datum Byte Bloat Attack (" [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ Int -> [Char]
forall a. Show a => a -> [Char]
show Int
inflatedSize [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ [Char]
" bytes)") (ThreatModel () -> ThreatModel ())
-> ThreatModel () -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$ do
[Output]
outputs <- ThreatModel [Output]
getTxOutputs
let scriptOutputsWithDatum :: [Output]
scriptOutputsWithDatum = (Output -> Bool) -> [Output] -> [Output]
forall a. (a -> Bool) -> [a] -> [a]
filter Output -> Bool
isScriptOutputWithInlineDatum [Output]
outputs
ThreatModel () -> ThreatModel ()
forall a. ThreatModel a -> ThreatModel a
threatPrecondition (ThreatModel () -> ThreatModel ())
-> ThreatModel () -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$ Bool -> ThreatModel ()
ensure (Bool -> Bool
not (Bool -> Bool) -> Bool -> Bool
forall a b. (a -> b) -> a -> b
$ [Output] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Output]
scriptOutputsWithDatum)
Output
target <- [Output] -> ThreatModel Output
forall a. Show a => [a] -> ThreatModel a
pickAny [Output]
scriptOutputsWithDatum
ScriptData
originalDatum <- case Output -> Maybe ScriptData
getInlineDatum Output
target of
Maybe ScriptData
Nothing -> [Char] -> ThreatModel ScriptData
forall a. [Char] -> ThreatModel a
failPrecondition [Char]
"Script output missing inline datum"
Just ScriptData
originalDatum' -> ScriptData -> ThreatModel ScriptData
forall a. a -> ThreatModel a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ScriptData
originalDatum'
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)
[Char] -> ThreatModel ()
counterexampleTM ([Char] -> ThreatModel ()) -> [Char] -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
[[Char]] -> [Char]
paragraph
[ [Char]
"The transaction contains a script output with an inline datum."
, [Char]
"Testing if the first item in list fields can be inflated to"
, Int -> [Char]
forall a. Show a => a -> [Char]
show Int
inflatedSize
, [Char]
"bytes while still passing validation."
]
[Char] -> ThreatModel ()
counterexampleTM ([Char] -> ThreatModel ()) -> [Char] -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
[[Char]] -> [Char]
paragraph
[ [Char]
"If this validates, the script doesn't limit ByteString field sizes,"
, [Char]
"enabling a datum bloat DoS attack where an attacker can add"
, [Char]
"a huge message/data item to bloat the datum beyond spendable limits."
]
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)
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