#37191 [BC-Insight] Unvalidated Field Names in Tuple ABI Parsing Causes Runtime Panic via reflect.StructOf

Submitted on Nov 28th 2024 at 03:23:27 UTC by @CertiK for Attackathon | Ethereum Protocol

  • Report ID: #37191

  • Report Type: Blockchain/DLT

  • Report severity: Insight

  • Target: https://github.com/ledgerwatch/erigon

  • Impacts:

    • (Specifications) A bug in specifications with no direct impact on client implementations

Description

Brief/Intro

A vulnerability exists in the accounts/abi package where unvalidated fieldName from user-provided ABI definitions are used to construct struct types using reflect.StructOf. This can cause a panic when the invalid characters are included in the fieldName.

Vulnerability Details

In the type.go file of the accounts/abi package, the code dynamically constructs tuple types based on ABI component definitions as follow:

https://github.com/erigontech/erigon/blob/v2.60.10/accounts/abi/type.go#L158

	case "tuple":
		var (
			fields     []reflect.StructField
			elems      []*Type
			names      []string
			expression string // canonical parameter expression
		)
		expression += "("
		overloadedNames := make(map[string]string)
		for idx, c := range components {
			cType, err := NewType(c.Type, c.InternalType, c.Components)
			if err != nil {
				return Type{}, err
			}
			fieldName, err := overloadedArgName(c.Name, overloadedNames)
			if err != nil {
				return Type{}, err
			}
			overloadedNames[fieldName] = fieldName
			fields = append(fields, reflect.StructField{
				Name: fieldName, // reflect.StructOf will panic for any exported field.
				Type: cType.GetType(),
				Tag:  reflect.StructTag("json:\"" + c.Name + "\""),
			})
			elems = append(elems, &cType)
			names = append(names, c.Name)
			expression += cType.stringKind
			if idx != len(components)-1 {
				expression += ","
			}
		}
		expression += ")"

		typ.TupleType = reflect.StructOf(fields)
		typ.TupleElems = elems
		typ.TupleRawNames = names
		typ.T = TupleTy
		typ.stringKind = expression

The fieldName is taken directly from the ABI component's Name without validation. However, the reflect.StructOf function requires that all field names be valid with the following check. Otherwise, the panic will rise.

https://github.com/golang/go/blob/master/src/reflect/type.go#L2205

		if !isValidFieldName(field.Name) {
			panic("reflect.StructOf: field " + strconv.Itoa(i) + " has invalid name")
		}

---

// isValidFieldName checks if a string is a valid (struct) field name or not.
//
// According to the language spec, a field name should be an identifier.
//

// identifier = letter { letter | unicode_digit } .
// letter = unicode_letter | "_" .
func isValidFieldName(fieldName string) bool {
	for i, c := range fieldName {
		if i == 0 && !isLetter(c) {
			return false
		}

		if !(isLetter(c) || unicode.IsDigit(c)) {
			return false
		}
	}

	return len(fieldName) > 0
}

It is worth noted a similar issue has been fixed in go-ethereum: https://github.com/ethereum/go-ethereum/pull/24932

Impact Details

Any program that invokes the vulnerable code will panic with an invalid field name.

References

  • https://github.com/golang/go/blob/master/src/reflect/type.go#L2205

  • https://github.com/ethereum/go-ethereum/pull/24932

Proof of Concept

Proof of Concept

We can reuse the test from go-ethereum to verify the issue:

// TestCrashers contains some strings which previously caused the abi codec to crash.
func TestCrashers(t *testing.T) {
	abi.JSON(strings.NewReader(`[{"inputs":[{"type":"tuple[]","components":[{"type":"bool","name":"_1"}]}]}]`))
	abi.JSON(strings.NewReader(`[{"inputs":[{"type":"tuple[]","components":[{"type":"bool","name":"&"}]}]}]`))
	abi.JSON(strings.NewReader(`[{"inputs":[{"type":"tuple[]","components":[{"type":"bool","name":"----"}]}]}]`))
	abi.JSON(strings.NewReader(`[{"inputs":[{"type":"tuple[]","components":[{"type":"bool","name":"foo.Bar"}]}]}]`))
}

Output:

--- FAIL: TestCrashers (0.00s)
panic: reflect.StructOf: field 0 has invalid name [recovered]
	panic: reflect.StructOf: field 0 has invalid name

goroutine 54 [running]:
testing.tRunner.func1.2({0x105c183c0, 0x140011f8710})
	/Users/xxx/.goenv/versions/1.22.5/src/testing/testing.go:1631 +0x1c4
testing.tRunner.func1()
	/Users/xxx/.goenv/versions/1.22.5/src/testing/testing.go:1634 +0x33c
panic({0x105c183c0?, 0x140011f8710?})
	/Users/xxx/.goenv/versions/1.22.5/src/runtime/panic.go:770 +0x124
reflect.StructOf({0x14000ad80e0, 0x1, 0x140016e4102?})
	/Users/xxx/.goenv/versions/1.22.5/src/reflect/type.go:2184 +0x1f14
github.com/erigontech/erigon/accounts/abi.NewType({0x140016e4118, 0x5}, {0x0, 0x0}, {0x140006d05a0, 0x1, 0x1})
	/Users/xxx/hackathon/erigon/accounts/abi/type.go:195 +0x1180
github.com/erigontech/erigon/accounts/abi.NewType({0x140016e4118, 0x7}, {0x0, 0x0}, {0x140006d05a0, 0x1, 0x1})
	/Users/xxx/hackathon/erigon/accounts/abi/type.go:90 +0x144
github.com/erigontech/erigon/accounts/abi.(*Argument).UnmarshalJSON(0x14000a36090, {0x14000ba020c, 0x3d, 0x1f4})
	/Users/xxx/hackathon/erigon/accounts/abi/argument.go:56 +0xc0
encoding/json.(*decodeState).object(0x14000a36000, {0x105d8f0e0?, 0x14000a36090?, 0x1046bd148?})
	/Users/xxx/.goenv/versions/1.22.5/src/encoding/json/decode.go:604 +0x5c4
encoding/json.(*decodeState).value(0x14000a36000, {0x105d8f0e0?, 0x14000a36090?, 0x1?})
	/Users/xxx/.goenv/versions/1.22.5/src/encoding/json/decode.go:374 +0x40
encoding/json.(*decodeState).array(0x14000a36000, {0x105bf55e0?, 0x14000ad8020?, 0x105c183c0?})
	/Users/xxx/.goenv/versions/1.22.5/src/encoding/json/decode.go:555 +0x48c
encoding/json.(*decodeState).value(0x14000a36000, {0x105bf55e0?, 0x14000ad8020?, 0x6?})
	/Users/xxx/.goenv/versions/1.22.5/src/encoding/json/decode.go:364 +0x70
encoding/json.(*decodeState).object(0x14000a36000, {0x105e51e80?, 0x14000ad8000?, 0x1046bd148?})
	/Users/xxx/.goenv/versions/1.22.5/src/encoding/json/decode.go:755 +0xa94
encoding/json.(*decodeState).value(0x14000a36000, {0x105e51e80?, 0x14000ad8000?, 0x1?})
	/Users/xxx/.goenv/versions/1.22.5/src/encoding/json/decode.go:374 +0x40
encoding/json.(*decodeState).array(0x14000a36000, {0x105bfb800?, 0x140016d9578?, 0x1046ca1ac?})
	/Users/xxx/.goenv/versions/1.22.5/src/encoding/json/decode.go:555 +0x48c
encoding/json.(*decodeState).value(0x14000a36000, {0x105bfb800?, 0x140016d9578?, 0x1046c9770?})
	/Users/xxx/.goenv/versions/1.22.5/src/encoding/json/decode.go:364 +0x70
encoding/json.(*decodeState).unmarshal(0x14000a36000, {0x105bfb800?, 0x140016d9578?})
	/Users/xxx/.goenv/versions/1.22.5/src/encoding/json/decode.go:181 +0x104
encoding/json.Unmarshal({0x14000ba0200, 0x4c, 0x200}, {0x105bfb800, 0x140016d9578})
	/Users/xxx/.goenv/versions/1.22.5/src/encoding/json/decode.go:108 +0xe4
github.com/erigontech/erigon/accounts/abi.(*ABI).UnmarshalJSON(0x14000cca008, {0x14000ba0200, 0x4c, 0x200})
	/Users/xxx/hackathon/erigon/accounts/abi/abi.go:160 +0x60
encoding/json.(*decodeState).array(0x14000a5e028, {0x105e44000?, 0x14000cca008?, 0x1046ccfe4?})
	/Users/xxx/.goenv/versions/1.22.5/src/encoding/json/decode.go:507 +0x370
encoding/json.(*decodeState).value(0x14000a5e028, {0x105e44000?, 0x14000cca008?, 0x1046cccec?})
	/Users/xxx/.goenv/versions/1.22.5/src/encoding/json/decode.go:364 +0x70
encoding/json.(*decodeState).unmarshal(0x14000a5e028, {0x105e44000?, 0x14000cca008?})
	/Users/xxx/.goenv/versions/1.22.5/src/encoding/json/decode.go:181 +0x104
encoding/json.(*Decoder).Decode(0x14000a5e000, {0x105e44000, 0x14000cca008})
	/Users/xxx/.goenv/versions/1.22.5/src/encoding/json/stream.go:73 +0x118
github.com/erigontech/erigon/accounts/abi.JSON({_, _})
	/Users/xxx/hackathon/erigon/accounts/abi/abi.go:55 +0x9c
github.com/erigontech/erigon/accounts/abi/bind_test.TestCrashers(0x140001b8820?)
	/Users/xxx/hackathon/erigon/accounts/abi/bind/base_test.go:265 +0x60
testing.tRunner(0x140001b8820, 0x105ef6850)
	/Users/xxx/.goenv/versions/1.22.5/src/testing/testing.go:1689 +0xec
created by testing.(*T).Run in goroutine 1
	/Users/xxx/.goenv/versions/1.22.5/src/testing/testing.go:1742 +0x318
FAIL	github.com/erigontech/erigon/accounts/abi/bind	0.929s
FAIL

Was this helpful?