Security · Original reporting
A Second gossipsub Package Has the Same Unguarded Check and No Advisory
A libp2p advisory published last month describes a gossipsub flaw that let anyone publish messages signed as somebody else, as long as the victim's peer ID was an RSA one. The patch landed in July. This desk read the code and found the identical line, under the same unanswered TODO comment, still shipping in an older package that no advisory covers — one npm served more often last week than the fixed one.
Gossipsub is the message layer underneath a great deal of peer-to-peer software: IPFS, Filecoin, several Ethereum clients, and a long tail of applications that need to flood a message across a network without a server in the middle. Its default setting, the one you get if you configure nothing, is called StrictSign. Every message carries the sender's peer ID, a signature, and the public key that signature belongs to. Receiving nodes verify all three before passing the message on. That is the whole promise of the mode: message.from means something.
On August 14 the libp2p project published GHSA-c3gv-825q-fvmp, CVE-2026-86038, rated 7.5 and titled "Gossipsub StrictSign accepts attacker-signed messages as a victim RSA peer ID." For a specific class of identity, the promise did not hold. An attacker could put a victim's peer ID in the from field, sign the message with their own key, attach their own public key, and watch the network accept and forward it as the victim's. The advisory credits the report to a researcher going by Alleysira. No outlet appears to have written about it. GitHub's record shows the advisory was last updated on September 17.
The line that only ran for some peer IDs
The flaw sat in one conditional in validateToRawMessage, in a file called buildRawMessage.ts. Before the fix, the check that bound the attached public key to the claimed sender read like this, with a comment its author left above it:
// TODO: Should `fromPeerId.pubKey` be optional?
if (fromPeerId.publicKey !== undefined && !publicKey.equals(fromPeerId.publicKey)) {
Read the condition from the left. The comparison never happens unless fromPeerId.publicKey is already defined. If the claimed sender's peer ID does not carry a public key of its own, the code has nothing to compare against, so it compares nothing and proceeds to verify the signature — against the key the sender supplied. A signature made with the attacker's key verifies correctly against the attacker's key. The message passes.
The commit that fixed it, #3569, states the problem in one sentence: the key "was checked against the from peer id by comparing public keys directly, which only applies to peer ids that inline their public key." The replacement derives an identity from the supplied key and compares that instead — if (!fromPeerId.equals(publicKey.toMultihash().bytes)) — so the check now applies to every peer ID type.
Why RSA was the exception
Which peer IDs inline their public key is not an accident of implementation; it is a size decision made years ago and documented at the time. A libp2p peer ID is a multihash of a public key. Small keys — Ed25519, secp256k1 — fit inside the identifier itself, so the key travels with the ID. RSA keys are too bulky, so an RSA peer ID is a SHA-256 digest of the key and nothing more. You cannot recover the key from the ID.
IPFS spelled this out when it switched defaults. Kubo's v0.7.0 changelog, from September 2020, explains that ed25519 keys "have an inline public key," which "means that someone only needs your PeerId to verify things you've signed," and so "we don't have to worry about storing those bulky RSA public keys." That is a precise description of the property the validator later assumed every peer ID had. Nodes created before that switch, and any node that kept its original identity, still have RSA peer IDs. Those were the identities that could be worn by anyone.
Four years of an unanswered TODO
| Date | Event |
|---|---|
| Sep 22, 2020 | Kubo v0.7.0 makes ed25519 the default key type, citing inline public keys |
| May 17, 2022 | The check and its TODO are already present in ChainSafe's gossipsub repo |
| Sep 22, 2025 | Gossipsub moves into the js-libp2p monorepo, the line carried over intact |
| Sep 24, 2025 | @libp2p/gossipsub 15.0.0 published — start of the advisory's affected range |
| Jul 17, 2026 | PR #3569 replaces the check so it applies to every peer ID type |
| Jul 19, 2026 | PR #3578 adds rejection for malformed keys and signatures; 16.0.5 published hours later |
| Aug 14, 2026 | GHSA-c3gv-825q-fvmp published, 26 days after the patch shipped |
buildRawMessage.ts in both repositories, and the advisory record. Source files checked on September 17.The fix that needed a fix
The repair did not land cleanly on the first try, and the commit messages are unusually candid about why. A first attempt derived the peer ID from the key using a helper that, per the second commit in the same pull request, "throws UnsupportedKeyTypeError for ECDSA keys, which escaped the validator and skipped the invalid-message score penalty." Comparing the derived multihash instead is defined for every key type, so that is what shipped.
That phrase — skipped the score penalty — points at a second, quieter problem. Gossipsub scores its peers, and a node that sends invalid messages is supposed to lose reputation. Two days later, PR #3578 noted that malformed key bytes or a wrong-length signature "made validateToRawMessage throw and escape to the error path instead of being rejected as invalid." A peer sending garbage was crashing out of the validator rather than being marked down for it. Both paths are now wrapped so they return a clean rejection, which the commit says matches the behavior of go-libp2p-pubsub and rust-libp2p.
The test added alongside the fix is worth reading, because it is the clearest statement of what was possible. It generates an RSA key for a victim and an Ed25519 key for an attacker, builds a message that claims the victim's peer ID while signing with the attacker's key, asserts that the from field really does hold the victim's identity, and then asserts that validation now returns InvalidPeerId. Before July, that last assertion would have failed.
The copy with no advisory
Gossipsub's JavaScript implementation has moved house. It was maintained for years by ChainSafe as @chainsafe/libp2p-gossipsub, then folded into the libp2p monorepo and published under a new name, @libp2p/gossipsub, starting with version 15.0.0 in September 2025. The advisory's affected range begins there. Everything published under the old name sits outside it.
So this desk checked the old name. The last release of @chainsafe/libp2p-gossipsub is 14.1.2, published on September 18, 2025. We downloaded that tarball from the npm registry and read the compiled file it ships. The check is there, unchanged, with the same TODO comment sitting above it. The project's public repository still carries the identical line on its default branch. Querying OSV, the open vulnerability database that aggregates GitHub's advisories, returns three known vulnerabilities for @libp2p/gossipsub and an empty result for @chainsafe/libp2p-gossipsub.
The download numbers make the gap concrete rather than theoretical. In the week of September 5 to 11, npm served @chainsafe/libp2p-gossipsub 19,086 times and @libp2p/gossipsub 16,755 times. The package with no advisory was fetched more often than the one that got patched. Those counts measure distribution — they include continuous integration runs, mirrors and transitive installs, and they are not a headcount of live nodes. But an unmaintained package pulling nearly twenty thousand weekly downloads is not a package nobody uses, and nothing in the advisory system tells any of those installs that the code they just fetched contains the line the CVE describes.
Who this does not reach
The blast radius is narrower than "everything built on libp2p," and it is worth being exact about where it stops. The attack needs a victim whose peer ID is an RSA one, which since 2020 has meant older identities rather than new ones. It also needs an application that actually trusts message.from — for authorization, moderation, accounting or audit logs. Software that treats gossipsub purely as a transport and authenticates at a higher layer loses nothing here.
Ethereum's consensus layer is structurally out of scope, for a reason that predates this bug. The consensus specifications require clients to run gossipsub under StrictNoSign, omitting the from, seqno, signature and key fields entirely and identifying messages by a hash of their contents. There is no sender field to forge. Lodestar, the JavaScript beacon node and the most likely consumer of this code in Ethereum, currently depends on @libp2p/gossipsub version 17.1.0 — well past the 16.0.5 that carries the fix. We found no evidence that any chain was attacked this way, and this desk is not suggesting one was.
The Take
The bug itself is an honest one. A validator assumed every identity carries its own key, which is true of the two key types almost everyone now uses and false of the one that came before them, and the author of that line flagged the doubt in a TODO rather than resolving it. The fix is correct, the follow-up patch two days later shows people actually reviewing their own work, and the maintainers wrote commit messages that explain their mistakes instead of hiding them. That is a project behaving well. The failure is downstream of all of it. Advisories are attached to package names, and this package changed its name, so the record now says one thing is dangerous and says nothing at all about a byte-identical copy that npm hands out more often. Nobody did anything wrong to produce that outcome, which is exactly why it will keep happening. A rename is a routine act of maintenance; it should not be able to quietly reset a package's security history to zero. The people best placed to close this particular gap are the libp2p and ChainSafe maintainers, who could request that the advisory's affected range name the old package too — a small filing that would put a warning in front of roughly nineteen thousand downloads a week that currently get none.