Skip to content

fix: add cycle detection to EIP-712 typed data encoding to prevent stack overflow DoS - #210

Merged
pkieltyka merged 1 commit into
masterfrom
fix/eip712-cycle-detection
Jun 19, 2026
Merged

fix: add cycle detection to EIP-712 typed data encoding to prevent stack overflow DoS#210
pkieltyka merged 1 commit into
masterfrom
fix/eip712-cycle-detection

Conversation

@patrislav

Copy link
Copy Markdown
Member

Summary

  • Cyclic EIP-712 type definitions (e.g. A→B→A) cause infinite recursion in EncodeType/encodeValue, leading to an unrecoverable stack overflow (recover() cannot catch it — the process dies)
  • Add ValidateTypeGraph() using DFS with grey-node cycle detection, called at two entry points:
    • UnmarshalJSON — rejects malicious payloads before any recursive traversal
    • Encode — catches programmatically constructed cyclic types
  • Array type references (e.g. B[]) are handled by stripping the suffix before graph traversal
  • Valid DAGs (e.g. diamond-shaped type graphs with shared subtypes) continue to work correctly

Test plan

  • Simple cycle: A→B→A (errors)
  • Self-referencing type: A→A (errors)
  • Longer cycle: A→B→C→A (errors)
  • Cycle through array types: A→B[]→A (errors)
  • Valid DAG with diamond shape: A→B, A→C, B→D, C→D (passes)
  • Valid simple types with no custom subtypes (passes)
  • Cycle rejected during JSON unmarshal path (errors)
  • Cycle rejected during Encode/EncodeDigest path (errors)
  • All existing tests pass unchanged

…ack overflow DoS

Cyclic type definitions (e.g. A→B→A) caused infinite recursion in
EncodeType/encodeValue, leading to an unrecoverable stack overflow.
This was reachable via unauthenticated endpoints with a ~200-byte payload.

Add ValidateTypeGraph() with DFS cycle detection, called in both
UnmarshalJSON and Encode to reject cycles before any recursion begins.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@patrislav
patrislav requested a review from a team June 19, 2026 11:18

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9608992df0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ethcoder/typed_data.go
Comment on lines +29 to +30
for typeName := range t {
if err := t.walkTypeGraph(typeName, make(map[string]bool)); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate only reachable EIP-712 types

Because this loops over every entry in Types, Encode and JSON unmarshalling now reject cycles that are never reachable from PrimaryType or EIP712Domain. For example, a signable payload with primary type Mail plus an unused A -> B -> A helper definition now fails before encoding, even though the existing encoder only traverses the domain and primary-type dependency graph and the unused types cannot cause recursion. Limit the DFS roots to the types actually encoded so irrelevant definitions do not break otherwise valid typed data.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreachable types are irrelevant, I don't think we need to make an exception for them. Open to changing this though, if needed.

@pkieltyka
pkieltyka merged commit b9c76cc into master Jun 19, 2026
14 checks passed
@pkieltyka
pkieltyka deleted the fix/eip712-cycle-detection branch June 19, 2026 15:04
pkieltyka pushed a commit that referenced this pull request Aug 17, 2026
…formed types (#214)

* fix: harden EIP-712 typed data encoding against amplification and malformed types

Follow-up to #210. Retesting surfaced a value-driven amplification path that the
schema-only cycle check does not cover, plus a panic reachable from any caller
that decodes untrusted typed data.

- Memoize EncodeType/TypeHash per Encode call. HashStruct previously recomputed
  a type's hash for every array element, so a message holding an array of custom
  structs multiplied schema-processing cost by the element count. The cache is
  shared across the whole call tree, domain and message alike, so type-dependent
  work happens at most once per distinct type.

- Add functional Options bounding both the schema and the message: WithMaxTypes,
  WithMaxFieldsPerType, WithMaxWalkVisits, WithMaxArrayElements,
  WithMaxRecursionDepth and WithMaxTotalValues. The zero value means unlimited,
  so existing callers are unaffected. Value-driven checks run before allocating
  or recursing, so an oversized array is rejected up front.

- Fix a panic in typedDataDecodePrimitiveValue. ABIUnmarshalStringValuesAny
  returns fewer values than requested, with a nil error, for a type token it does
  not recognize; the caller then indexed out[0] and panicked on input as simple
  as {"type": ""} or {"type": "foobar"}.

- Make ValidateTypeGraph's walk an explicit-stack DFS and cap nesting at
  maxTypeGraphDepth. The encoders below it still recurse one frame per level, and
  a long enough type chain overflowed the goroutine stack fatally. The ceiling is
  measured from each type's longest downward path rather than the live DFS stack,
  which memoization can cut short depending on map iteration order.

- Reject field types no encoder can handle (unknown names, uint0/uint7/uint2560,
  bytes0/bytes33, bare uint/int, malformed array suffixes) instead of letting
  them fail deep inside the encoders.

Note: schema validation is stricter than before. A schema declaring a type with
an invalid field type previously decoded and only failed if that type was
actually encoded; it is now rejected at decode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(ethcoder): trim typed-data comments to non-obvious rationale

Drop comments that restated what the code already shows, and compress the rest
to the reason a reader cannot infer: why the depth ceiling is unconditional, why
depth is tracked separately from the live DFS stack, why bare uint/int are
rejected, and why the budget is checked before allocating.

Removes the duplicated memoization rationale that appeared on encodeTypeCached,
hashStruct and Encode, keeping it only on encodeTypeCached.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(ethcoder): review follow-ups on typed-data hardening

- Rename Option to TypedDataOption: ethcoder already exports Options
  for merkle proofs, and the two were one letter apart.
- Guard encodeTypeCached against cycles via an in-progress sentinel,
  so EncodeType/TypeHash/HashStruct called directly (skipping
  ValidateTypeGraph) fail cleanly instead of overflowing the stack.
- Require canonical width spellings in isPrimitiveType: uint0256,
  bytes01 etc. matched the regex but aren't valid EIP-712 types.
- Change budget option fields from int to uint so a negative value is
  a compile error instead of silently meaning unlimited.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants