Geth Node Vulnerable to DoS via maliciously crafted p2p message

Description

Impact

A vulnerable node is susceptible to crash when processing a maliciously crafted message from a peer, via the snap/1 protocol. The crash can be triggered by sending a malicious snap/1 GetTrieNodes package.

Details

On September 21, 2021, geth-team member Gary Rong (@rjl493456442) found a way to crash the snap request handler .
By using this vulnerability, a peer connected on the snap/1 protocol could cause a vulnerable node to crash with a panic.

In the trie.TryGetNode implementation, if the requested path is reached, the associated node will be returned. However the nilness is
not checked there.

func (t *Trie) tryGetNode(origNode node, path []byte, pos int) (item []byte, newnode node, resolved int, err error) {
    // If we reached the requested path, return the current node
    if pos >= len(path) {
        // Although we most probably have the original node expanded, encoding
        // that into consensus form can be nasty (needs to cascade down) and
        // time consuming. Instead, just pull the hash up from disk directly.
        var hash hashNode
        if node, ok := origNode.(hashNode); ok {
            hash = node
        } else {
            hash, _ = origNode.cache()
        }

More specifically the origNode can be nil(e.g. the child of fullnode) and system can panic at line hash, _ = origNode.cache().

When investigating this, @holiman tried to find it via fuzzing, which uncovered a second crasher, also related to the snap GetTrieNodes package. If the caller requests a storage trie:

                // Storage slots requested, open the storage trie and retrieve from there
                account, err := snap.Account(common.BytesToHash(pathset[0]))
                loads++ // always account database reads, even for failures
                if account == nil {
                    break
                }
                stTrie, err := trie.NewSecure(common.BytesToHash(account.Root), triedb)

The code assumes that snap.Account returns either a non-nil response unless error is also provided. This is however not the case, since snap.Account can return nil, nil.

Patches

--- a/eth/protocols/snap/handler.go
+++ b/eth/protocols/snap/handler.go
@@ -469,7 +469,7 @@ func handleMessage(backend Backend, peer *Peer) error {
                // Storage slots requested, open the storage trie and retrieve from there
                account, err := snap.Account(common.BytesToHash(pathset[0]))
                loads++ // always account database reads, even for failures
-               if err != nil {
+               if err != nil || account == nil {
                    break
                }
                stTrie, err := trie.NewSecure(common.BytesToHash(account.Root), triedb)
diff --git a/trie/trie.go b/trie/trie.go
index 7ea7efa835..d0f0d4e2bc 100644
--- a/trie/trie.go
+++ b/trie/trie.go
@@ -174,6 +174,10 @@ func (t *Trie) TryGetNode(path []byte) ([]byte, int, error) {
 }

 func (t *Trie) tryGetNode(origNode node, path []byte, pos int) (item []byte, newnode node, resolved int, err error) {
+   // If non-existent path requested, abort
+   if origNode == nil {
+       return nil, nil, 0, nil
+   }
    // If we reached the requested path, return the current node
    if pos >= len(path) {
        // Although we most probably have the original node expanded, encoding
@@ -193,10 +197,6 @@ func (t *Trie) tryGetNode(origNode node, path []byte, pos int) (item []byte, new
    }
    // Path still needs to be traversed, descend into children
    switch n := (origNode).(type) {
-   case nil:
-       // Non-existent path requested, abort
-       return nil, nil, 0, nil
-
    case valueNode:
        // Path prematurely ended, abort
        return nil, nil, 0, nil

The fixes were merged into #23657, with commit f1fd963, and released as part of Geth v1.10.9 on Sept 29, 2021.

Workarounds

Apply the patch above or upgrade to a version which is not vulnerable.

For more information

If you have any questions or comments about this advisory:
* Open an issue in go-ethereum
* Email us at [email protected]

Basic information

Type
reviewed
Severity
medium
Advisory on GitHub
Open advisory ↗
Repository advisory
Open repository advisory ↗
Source code
Browse source ↗
Published (advisory)
2021-10-25 19:42:57 UTC
Updated
2023-08-29 18:55:40 UTC
GitHub reviewed
2021-10-25 18:23:10 UTC
NVD published
2021-10-26

EPSS Score

Score Percentile
0.18% 39.27%

CVSS Scores

Base score Version Severity Vector
5.7 3.1
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H Click to expand
Attack vector (AV:N)
Could be attacked over the internet or any normal routed network—not just someone sitting at the machine.
Attack complexity (AC:L)
Once they can reach the bug, pulling it off is straightforward—no weird race conditions or rare setup.
Privileges required (PR:L)
A normal user session is enough; they don’t have to be admin.
User interaction (UI:R)
A real person has to do something—click, install, enable—otherwise it doesn’t land.
Scope (S:U)
Damage stays in the same “trust bubble” as the broken component—no big spill into unrelated systems.
Confidentiality (C:N)
Doesn’t really leak secrets in a meaningful way.
Integrity (I:N)
Data isn’t meaningfully altered or forged.
Availability (A:H)
Could take the service down hard or make it unusable for people who depend on it.

Identifiers

CWEs

CWE id Name
CWE-20 Improper Input Validation

Credits

  • rjl493456442 (analyst)
  • holiman (analyst)

Affected packages (1)

Vulnerable version ranges and first patched releases as published by GitHub.

Ecosystem Package Vulnerable range First patched Vulnerable functions
go github.com/ethereum/go-ethereum < 1.10.9 1.10.9

References

cvelogic Threat Intelligence