convex-testing-interface
Safe HaskellSafe-Inferred
LanguageHaskell2010

Convex.ThreatModel.SelfReferenceInjection

Description

Threat model for detecting Self-Reference Injection vulnerabilities.

A Self-Reference Injection attack exploits validators that check "pay to address X" where X comes from the datum. If an attacker can set X to the script's own address, then the continuation output (which necessarily goes to the script) satisfies the payment check for free.

Consequences ==

  1. Free value extraction: The attacker bypasses payment requirements. Instead of paying to the intended recipient, the "payment" is just the continuation output that was going to the script anyway.
  2. Protocol violation: The semantic meaning of "pay to X" is violated. For example, in a "king of the hill" contract, the old king never receives their rightful payment when overthrown.

Vulnerable Patterns ==

validator spend(datum: Datum, _redeemer: Void, ctx: ScriptContext) {
  // Check that SOME output pays current_beneficiary
  expect Some(out) = list.find(ctx.transaction.outputs, fn(o) {
    o.address == datum.current_beneficiary && o.value >= datum.min_payment
  })
  ...
}

If datum.current_beneficiary can be set to the script's own address (either through initialization or a state transition), the continuation output satisfies this check automatically.

Attack Flow ==

  1. Set current_beneficiary (or similar address field) to the script address
  2. Create a transaction that spends the script UTxO
  3. The continuation output satisfies the "payment" check
  4. No real payment to the intended recipient occurs

Mitigation ==

  1. Validate address is different: Check that the beneficiary address is NOT the script's own address.
  2. Use credential type restrictions: Only allow PubKeyCredential for beneficiary fields (not ScriptCredential).
  3. External payment validation: Check that a SEPARATE output (not the continuation) pays the beneficiary.

This threat model: 1. Finds a script input (to identify the script address) 2. Finds continuation outputs with inline datums 3. Replaces address credentials in the datum with the script's own credential 4. Tests if the modified transaction still validates (vulnerability exists!)

Synopsis

Threat models

selfReferenceInjection :: ThreatModel () Source #

Check for Self-Reference Injection vulnerabilities.

For a transaction with script inputs and outputs containing inline datums:

  • Identifies the script address from the spent script input
  • Finds continuation outputs (script outputs with inline datums)
  • Replaces address credentials in the datum with the script's own credential
  • If the transaction still validates, the script is vulnerable to self-reference

This catches the "king of the hill" vulnerability where setting current_king to the script address allows bypassing the payment requirement.

The attack works because: 1. Script checks "some output pays current_king at least X" 2. When current_king = script_address, the continuation output satisfies this 3. The attacker gets the "king" status without actually paying anyone

selfReferenceInjectionWith :: Bool -> ThreatModel () Source #

Check for Self-Reference Injection with configurable verbosity.

selfReferenceInjectionWith True  -- Verbose mode with detailed counterexamples
selfReferenceInjectionWith False -- Standard mode

Datum transformation

injectScriptCredential :: ScriptData -> ScriptData -> ScriptData Source #

Inject a script credential into all address-like structures in a datum.

An address in Plutus is encoded as: Constr 0 [credential, staking_credential]

Where credential is either: - Constr 0 [Bytes pubkey_hash] (PubKeyCredential) - Constr 1 [Bytes script_hash] (ScriptCredential)

This function walks the datum and replaces all PubKeyCredential structures with the given ScriptCredential, simulating an attacker who sets the beneficiary address to the script's own address.

isAddressLikeStructure :: ScriptData -> Bool Source #

Check if a ScriptData structure looks like a Plutus Address.

A Plutus Address is: Constr 0 [credential, staking_credential]

Where: - credential is Constr 0|1 [Bytes (28 bytes)] - staking_credential is Constr 0|1 [...] or Constr 0 [] (None)