Skip to content

Commit 2e24436

Browse files
committed
feat(threat-model): Large Data Attack implementation
- Add largeDataAttack threat model detecting permissive FromData/UnsafeFromData parsers - Make PingPong.hs secure with strict UnsafeFromData (rejects extra fields) - Make Vulnerable.hs self-contained with unstableMakeIsData (vulnerable to both attacks) - Rename pingPongVulnerable* to vulnerablePingPong* - Move test groups to PingPongSpec.hs and BountySpec.hs
1 parent 86fd753 commit 2e24436

File tree

8 files changed

+483
-229
lines changed

8 files changed

+483
-229
lines changed

src/testing-interface/convex-testing-interface.cabal

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ library
4444
Convex.ThreatModel
4545
Convex.ThreatModel.Cardano.Api
4646
Convex.ThreatModel.DoubleSatisfaction
47+
Convex.ThreatModel.LargeData
4748
Convex.ThreatModel.Pretty
4849
Convex.ThreatModel.TxModifier
4950
Convex.ThreatModel.UnprotectedScriptOutput
@@ -95,7 +96,7 @@ test-suite convex-testing-interface-test
9596
Scripts.Bounty
9697
Scripts.Bounty.Vulnerable.DoubleSatisfaction
9798
Scripts.PingPong
98-
Scripts.PingPong.Vulnerable.UnprotectedScriptOutput
99+
Scripts.PingPong.Vulnerable
99100
Scripts.Sample
100101

101102
build-depends:
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
{- | Threat model for detecting Large Data Attack vulnerabilities.
4+
5+
A Large Data Attack exploits permissive @FromData@ parsers in Plutus validators
6+
that ignore extra fields when deserializing @Constr@ data. If a validator's
7+
datum parser only checks the fields it expects and ignores additional ones,
8+
an attacker can "bloat" the datum with extra fields while preserving the
9+
validator's interpretation.
10+
11+
== Consequences ==
12+
13+
1. __Increased execution costs__: Processing bloated datums wastes CPU/memory
14+
execution units, making transactions more expensive.
15+
16+
2. __Permanent fund locking__: If the datum is bloated sufficiently:
17+
18+
- Deserializing the datum may exceed execution unit limits
19+
- The transaction required to spend the UTxO may exceed protocol size limits
20+
- Min-UTxO requirements increase with datum size
21+
22+
In these cases, the UTxO becomes __permanently unspendable__ and funds
23+
are locked forever with no possibility of recovery.
24+
25+
== Root Cause ==
26+
27+
'unstableMakeIsData' and 'makeIsDataIndexed' generate parsers that use
28+
wildcard patterns for constructor fields:
29+
30+
@
31+
case (index, args) of
32+
(0, _) -> MyConstructor -- The "_" ignores ALL extra fields!
33+
@
34+
35+
This means @Constr 0 []@ and @Constr 0 [junk1, junk2, ..., junk10000]@ both
36+
parse to the same value, allowing attackers to inject arbitrary data.
37+
38+
== Mitigation ==
39+
40+
A secure validator should either:
41+
42+
- Use strict manual @FromData@ instances that check field count exactly
43+
- Validate the datum hash matches an expected value
44+
- Check datum structure explicitly in the validator logic
45+
46+
This threat model tests if a script output with an inline datum still validates
47+
when additional fields are appended to the datum's @Constr@ data structure.
48+
If it does, the validator has a Large Data Attack vulnerability.
49+
-}
50+
module Convex.ThreatModel.LargeData (
51+
largeDataAttack,
52+
largeDataAttackWith,
53+
bloatData,
54+
) where
55+
56+
import Convex.ThreatModel
57+
58+
{- | Check for Large Data Attack vulnerabilities with 1000 extra fields.
59+
60+
This is the default configuration that appends 1000 extra @ScriptDataNumber 42@
61+
fields to any inline datum on a script output. If the transaction still
62+
validates, the script's datum parser is permissive and vulnerable.
63+
-}
64+
largeDataAttack :: ThreatModel ()
65+
largeDataAttack = largeDataAttackWith 1000
66+
67+
{- | Check for Large Data Attack vulnerabilities with a configurable number
68+
of extra fields.
69+
70+
For a transaction with script outputs containing inline datums:
71+
72+
* Try bloating the datum by appending @n@ extra fields
73+
* If the transaction still validates, the script doesn't strictly validate
74+
its datum structure - it only checks expected fields and ignores extras.
75+
76+
This catches a vulnerability where different parsers may interpret the same
77+
on-chain data differently, leading to potential exploits.
78+
-}
79+
largeDataAttackWith :: Int -> ThreatModel ()
80+
largeDataAttackWith n = do
81+
-- Get all outputs from the transaction
82+
outputs <- getTxOutputs
83+
84+
-- Filter to script outputs with inline datums
85+
let scriptOutputsWithDatum = filter isScriptOutputWithInlineDatum outputs
86+
87+
-- Precondition: there must be at least one script output with inline datum
88+
threatPrecondition $ ensure (not $ null scriptOutputsWithDatum)
89+
90+
-- Pick a target output
91+
target <- pickAny scriptOutputsWithDatum
92+
93+
-- Extract the inline datum (we know it exists due to the filter)
94+
case getInlineDatum target of
95+
Nothing -> fail "Expected inline datum but found none"
96+
Just originalDatum -> do
97+
let bloatedDatum = bloatData n originalDatum
98+
99+
counterexampleTM $
100+
paragraph
101+
[ "The transaction contains a script output at index"
102+
, show (outputIx target)
103+
, "with an inline datum."
104+
]
105+
106+
counterexampleTM $
107+
paragraph
108+
[ "Testing if the datum can be bloated with"
109+
, show n
110+
, "extra fields while still passing validation."
111+
]
112+
113+
counterexampleTM $
114+
paragraph
115+
[ "If this validates, the script's FromData parser is permissive"
116+
, "and ignores extra Constr fields. An attacker could exploit this"
117+
, "to make a single datum satisfy multiple validators,"
118+
, "or to bypass certain datum-based checks."
119+
]
120+
121+
-- Try to validate with the bloated datum
122+
shouldNotValidate $ changeDatumOf target (toInlineDatum bloatedDatum)
123+
124+
{- | Bloat a @ScriptData@ value by appending extra fields to a @Constr@.
125+
126+
For @ScriptDataConstructor idx fields@, appends @n@ copies of
127+
@ScriptDataNumber 42@ to the fields list.
128+
129+
For other @ScriptData@ variants (Map, List, Number, Bytes), returns
130+
the value unchanged since they don't have the Constr structure that
131+
typical FromData instances parse.
132+
-}
133+
bloatData :: Int -> ScriptData -> ScriptData
134+
bloatData n sd = case sd of
135+
ScriptDataConstructor idx fields ->
136+
let extraFields = replicate n (ScriptDataNumber 42)
137+
in ScriptDataConstructor idx (fields ++ extraFields)
138+
-- Other cases: return unchanged
139+
_ -> sd
140+
141+
-- | Check if an output is a script output with an inline datum.
142+
isScriptOutputWithInlineDatum :: Output -> Bool
143+
isScriptOutputWithInlineDatum output =
144+
not (isKeyAddressAny (addressOf output)) && hasInlineDatum output
145+
146+
-- | Check if an output has an inline datum.
147+
hasInlineDatum :: Output -> Bool
148+
hasInlineDatum output =
149+
case datumOfTxOut (outputTxOut output) of
150+
TxOutDatumInline{} -> True
151+
_ -> False
152+
153+
-- | Extract the inline datum from an output if present.
154+
getInlineDatum :: Output -> Maybe ScriptData
155+
getInlineDatum output =
156+
case datumOfTxOut (outputTxOut output) of
157+
TxOutDatumInline _ hashableData -> Just (getScriptData hashableData)
158+
_ -> Nothing
159+
160+
-- | Convert a @ScriptData@ to an inline @Datum@ (TxOutDatum CtxTx Era).
161+
toInlineDatum :: ScriptData -> Datum
162+
toInlineDatum sd =
163+
TxOutDatumInline BabbageEraOnwardsConway (unsafeHashableScriptData sd)

src/testing-interface/test/BountySpec.hs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
{-# LANGUAGE ViewPatterns #-}
77

88
module BountySpec (
9+
-- * Test tree
10+
bountyTests,
11+
912
-- * Property tests
1013
propBountyVulnerableToDoubleSatisfaction,
1114
propBountySecureAgainstDoubleSatisfaction,
@@ -47,15 +50,30 @@ import Convex.Wallet (addressInEra, verificationKeyHash)
4750
import Convex.Wallet.MockWallet qualified as Wallet
4851
import Scripts qualified
4952
import Test.QuickCheck.Monadic (monadicIO, monitor, run)
53+
import Test.Tasty (TestTree, testGroup)
5054
import Test.Tasty.QuickCheck (
5155
Property,
5256
counterexample,
57+
testProperty,
5358
)
5459
import Test.Tasty.QuickCheck qualified as QC
5560

5661
plutusScript :: (C.IsPlutusScriptLanguage lang) => C.PlutusScript lang -> C.Script lang
5762
plutusScript = C.PlutusScript C.plutusScriptVersion
5863

64+
-- | All Bounty tests grouped together
65+
bountyTests :: RunOptions -> TestTree
66+
bountyTests opts =
67+
testGroup
68+
"bounty (double satisfaction)"
69+
[ testProperty
70+
"Bounty VULNERABLE to double satisfaction"
71+
(propBountyVulnerableToDoubleSatisfaction opts)
72+
, testProperty
73+
"Bounty SECURE against double satisfaction"
74+
(propBountySecureAgainstDoubleSatisfaction opts)
75+
]
76+
5977
{- | Test that demonstrates the VULNERABLE bounty's vulnerability to double satisfaction.
6078
6179
This test runs the doubleSatisfaction threat model against the VULNERABLE

0 commit comments

Comments
 (0)