bee-js 13.3.0 had valid provenance, a real release bot, and a wallet stealer. GitHub Actions did exactly what it was told
on November 9 someone used Ethersphere’s own GitHub release automation to publish a malicious @ethersphere/bee-js 13.3.0 to npm and a trojanized, Apple-notarized Swarm Desktop 0.56.2. no npm token was stolen. none was needed. the legitimate workflow, with trusted publishing and genuine SLSA provenance, built and signed the attacker’s commit. every badge was green. every badge was technically correct. this is the GitHub Tools column, and today the tool is your CI.
@ethersphere/[email protected] on npm (live about 75 minutes, 00:09:50 to 01:24:58 UTC on November 9, per Socket) and Swarm Desktop 0.56.2 on GitHub (public about eight and a half hours). good: bee-js 13.2.0 and Swarm Desktop 0.56.3. the npm package added a postinstall hook that fetched and ran JavaScript from api[.]frameworkmetrics[.]xyz; later stages stole the Swarm node’s config.yaml and swarm.key and installed persistence. GitHub advisory: GHSA-hh7j-5m3j-23pf. Socket ties the infrastructure to the earlier SleepyDuck / EtherDuck Open VSX campaign through a shared Ethereum smart contract.what got hit
@ethersphere/bee-js is the JavaScript client for Bee, the node software of the Swarm decentralized storage network. Socket puts it at about 470 npm downloads a day in early November, used by Ethersphere’s own swarm-cli, bee-dashboard and create-swarm-app, plus third-party Swarm apps. Swarm Desktop is the Electron app that installs and runs a Bee node, including the node’s Ethereum wallet.
small numbers, high value targets: developers who keep crypto wallets on the same machine as their dependency tree. which is to say, developers.
how it got in, per Socket
Socket’s analysis (November 9) says two GitHub accounts were compromised: nugaon, a long-time maintainer who still had force-push rights to an unprotected master on swarm-cli, and bee-worker, the release automation identity. Socket says the origin of both compromises predates November 6 “and remains unexplained.” StepSecurity agrees the evidence points to misuse of the bee-worker identity and says it “does not establish which credential was used or how it was obtained.” so: we do not know how the keys walked out. nobody does yet.
there was a dress rehearsal. on November 6, the same actor pushed workflows that dumped the full secrets and vars contexts to webhook[.]site collectors, then edited swarm-cli’s existing check.yaml eleven times in 72 minutes, narrowing from all secrets to PATs, then code-signing material, then NPM_TOKEN. Socket assesses “with moderate confidence that nothing was exfiltrated” from that attempt. three days later the actor stopped trying to steal the publishing credentials and simply used the publishing pipeline.
the npm side: a version script that learned to phone home
an unsigned commit, de2f74e, authored as bee-worker and titled to look like a release-please release (“chore(upcoming): release 13.3.0”), changed four files. the important edits, per both Socket and StepSecurity:
package.jsonaddedscriptsto the shippedfileslist and renamed the existingpreparehook topostinstall. Socket notes this was the first install-time hook in the package’s history across 177 other versions.scripts/generate-version.cjsgained a block at the top that requests/v1/analyticsfromapi[.]frameworkmetrics[.]xyzwith the OS name as the User-Agent and anX-Ethersphere: 1header, then runs whatever comes back throughnew Functionwith realrequireandprocess.
the comments called it an “analytics ping.” the request has no environment, CI, or time check. importing the library is not required. installing it is enough, if install scripts run.
which clients run them? per Socket: npm 11 and earlier, Yarn Classic, and Yarn Berry before 4.14.0 run dependency postinstall scripts by default. npm 12, pnpm 10 and later, Yarn Berry 4.14.0 and later, Bun and Deno skip unreviewed dependency scripts by default. if your CI image is on npm 11, it ran.
one sharp detail: Socket found that a failed install is not evidence you were spared. request-level errors are swallowed, but errors after the response starts become an unhandled rejection that fails the install with exit code 1. by then the request has already gone out.
the CI side: the part every maintainer should read twice
at 00:06:01 UTC on November 9, bee-worker published a GitHub release for an unsigned tag v13.3.0 pointing at de2f74e. that release event started the project’s normal publish workflow. Socket’s read of that workflow:
- tests, checks and entrypoint jobs ran only on
push. thepublishjob explicitly acceptedreleaseevents with those jobs skipped. - nothing checked that the released commit was on the protected
upcomingbranch. - the
publishenvironment had no required reviewers and no tag policy. “The deployment went from creation to success in 73 seconds with no approval step.” - because the build calls the rewritten script, the loader ran three times inside the publishing job, which had
id-token: writefor trusted publishing. the C2 answered the runner with JavaScript. what it did beyond printing a line “cannot be determined from public data.”
and the provenance? Socket verified it end to end, offline: signatures, Fulcio chain, SCT, Rekor inclusion, tarball digest. all genuine. the certificate correctly names the workflow, the tag, the commit, the trigger and the environment. “The legitimate 13.2.0 attestation has exactly the same shape.” Socket’s conclusion is the sentence of the week: provenance “proves where a package was built, not that the source was reviewed.”
we have spent two years telling people to “check provenance.” that advice was not wrong. it was incomplete, and this is what incomplete looks like.
the desktop side: signed, notarized, auto-updated
about 30 minutes later, the same identity created a branch emergency/0.56.2, published a full (not draft) GitHub release v0.56.2 titled “emergency patch release,” and dispatched pre-release.yml, a manually triggered workflow that takes a free-form branch input, has no environment protection, and hands the build the Apple and Windows signing certificates and the Apple ID used for notarization. per Socket, that workflow had been broken since March because it never installed pnpm. the attacker fixed it. the fix commit carries a Co-authored-by: Cursor trailer and was pushed through the nugaon account. we will leave that one there.
the result: every 0.56.2 package Socket unpacked had the same loader in the Electron main process, running at every launch before anything else. the macOS build was signed with the project’s Developer ID and notarized by Apple. Swarm Desktop uses update-electron-app, which ignores drafts and pre-releases, which is why publishing a full release mattered: Windows x64 and Apple Silicon users could be offered 0.56.2 automatically. Socket counted download counters at 65 by 08:27 UTC, an upper bound that includes researchers.
deleting a release does not roll anyone back. machines on 0.56.2 stayed there until 0.56.3 shipped.
what the later stages do
summarized from Socket and StepSecurity. we are describing behavior for detection, not reproducing code:
- stage 2: with the gate header, the server returns per-OS JavaScript that looks for the Swarm Desktop data directory and posts
config.yamlanddata-dir/keys/swarm.keyto/v1/swarm/report. the key is an encrypted Ethereum keystore. the config holds its password in plain text. together, that is the wallet. - stage 3, Linux and macOS: a Go backdoor in
~/.solidity/, launched by.run.pyor.run.cjs, persisted as systemd user unitsolidity-langsupport-runner.serviceor LaunchAgentcom.solidity.langsupport.runner. Socket says it reads its hub address from an Ethereum contract through public RPC gateways and gives the operator a shell. - stage 3, Windows: per Socket, a PowerShell loader that elevates, adds a Defender exclusion, and silently installs a genuine ConnectWise-signed ScreenConnect client configured for relay
185[.]232[.]84[.]169:8041, running as the serviceScreenConnect Client (5cbe79c9c25524df). StepSecurity, which did not retrieve the Windows binaries, was more cautious about the elevation mechanism. so are we.
the “Solidity language support” names make no sense for Swarm users. Socket says they carried over unchanged from the EtherDuck Open VSX campaign, along with the contract 0xf8a900Db50b3331be6B768bA460BB59f3E40C344, its owner wallet, six payload hashes, the ScreenConnect instance and the relay. the operator went from fake VS Code-style extensions you had to install yourself to a real project’s release pipeline. same targets. better distribution.
step 1: did it touch you?
the kit’s repos mode walks a directory of checked-out projects and flags any npm, Yarn or pnpm lockfile resolving @ethersphere/bee-js 13.3.0 or carrying the malicious tarball’s integrity hash. host mode checks a home directory for ~/.solidity, the systemd user unit, the LaunchAgent plist, and a bee-desktop.log line reading “Bee Desktop version: 0.56.2”, which Socket says each launch writes before the loader runs. netlog greps DNS, proxy or firewall logs for the C2 domain and the hub and relay addresses.

if anything hits, Socket’s and StepSecurity’s advice lines up: treat the machine or CI job as having run attacker code, isolate and rebuild from a trusted image, rotate every credential reachable from it (including cloud roles reachable through CI OIDC trust), and move Swarm node funds, chequebook and stake to a new key from a clean machine. removing the package does not remove the persistence.
step 2: audit your own pipeline before someone else does
here is the uncomfortable part for everyone who is not Ethersphere. the gaps Socket describes are common:
- a publish job that fires on a
releaseevent without checking the tagged commit is on a protected branch - tests that only run on
push, so the release path skips them - a publishing environment with no required reviewers and no tag restrictions
- a manually dispatched workflow that takes any branch name and receives signing certificates
- a release-bot token treated like a convenience instead of a publishing credential
the kit’s workflows mode is a deliberately dumb heuristic pass over .github/workflows that flags the first, fourth, and missing-environment cases. it cannot see your environment reviewers or rulesets, because those live in repository settings, so it tells you to go check. for a real static analysis of Actions workflows, run zizmor, an open-source GitHub Actions auditor, as well. it is better at this than our 60 lines of regex, and we are fine admitting it.

Socket’s maintainer recommendations, condensed, because they are good:
- do not let a release event publish without proof the commit is on a protected branch or came from a merged release PR. check it inside the publishing workflow.
- put the publishing environment behind required reviewers and restrict which tags can deploy to it. use rulesets to restrict tag and release creation.
- do not skip tests on the release path.
- do not expose signing certificates and notarization credentials to manually dispatched workflows that accept an arbitrary branch.
- scope, rotate and alert on the release bot’s token like the publishing credential it is.
step 3: consumer-side hardening
- turn off install scripts by default.
npm config set ignore-scripts trueat user or CI level, then allowlist the few packages that truly need them. or move to a client that already defaults to skipping them, per the list above. - pin exact versions for anything that touches wallets or secrets.
"^13.0.0"is how projects that never chose 13.3.0 got it anyway. StepSecurity lists swarm-cli 3.7.0 (^13.2.0) and bee-dashboard 0.37.1 (^13.0.0) as examples whose ranges admitted it. - purge caches and mirrors. Socket found a public npm mirror still serving the malicious tarball with matching checksums hours after npm removed it. your internal Artifactory probably did the same thing.
- keep using
npm audit signatures. it verifies registry signatures and provenance. just understand that 13.3.0 would have passed, because its provenance was real.
the industry part
supply-chain security spent the last few years building better locks for the front door: no long-lived tokens, OIDC, provenance, signed builds, notarization. all good. all present here. the attacker came in through the release bot and used the locks as a delivery service.
the boring controls are the ones that would have stopped it: branch protection on the branch you release from, a human approving the publish environment, tests that run on every path to production. none of those have a logo or a conference talk. all of them would have turned a 73-second deployment into a “why is there a release nobody approved” message.
download the kit
beejs-13-3-0-compromise-kit.zip contains beejs-check.py (repos, host, netlog and workflows modes, with the Socket and StepSecurity IOCs built in), beejs-harden.sh (user-level npm hardening, dry run by default), a README, and the synthetic lab files from the screenshots. good versions are printed in the output: bee-js 13.2.0, Swarm Desktop 0.56.3. offline, read-only, no exploit code.
if nobody on your team can say which of your workflows can publish or sign without a human approving it, that is a short review with a long payoff. Book a slot with Dark Edge Consulting.
indicators (defanged)
- packages:
@ethersphere/[email protected]; Swarm Desktop v0.56.2 - commits: bee-js
de2f74e784b2eafa0d66fbb6d88b42ea11ee34cf; Swarm Desktop2de727046706062d38edfbbf8340f32d1eb2bdfb,dc7d7455c95b3092eb14bff2bd93315b84b066fa - domain:
api[.]frameworkmetrics[.]xyz(paths/v1/analytics,/v1/electron,/v1/swarm/report,/v1/assets/) - hubs:
193[.]57[.]9[.]155:4912(current per Socket),193[.]57[.]9[.]17,178[.]16[.]54[.]222,107[.]189[.]27[.]223,107[.]189[.]27[.]46,91[.]108[.]240[.]156,107[.]189[.]16[.]215; ScreenConnect relay185[.]232[.]84[.]169:8041 - contract:
0xf8a900Db50b3331be6B768bA460BB59f3E40C344(getter selector0x02d91518returns the hub) - bee-js-13.3.0.tgz SHA-256:
d01fdf241aa9e0a1ba6ebc19f8f667af1bf652745021c5050126cb7036f7ce58 - Swarm Desktop 0.56.2
dist/desktop/src/index.jsSHA-256:f81315968e679b48f6bdff1c9b928bb223230d07f44d7948412c2250c2a14deb - stage 3
linux_amd64:9b73e7cd4e1425e770392549d8df46c706139ef476f4f7b9ac405165dc8d9696;linux_arm64:b601776817363b96295119f7221338a122d5247a35a77a64b04f286e1bbcc565 - persistence:
~/.solidity/,solidity-langsupport-runner.service,com.solidity.langsupport.runner, serviceScreenConnect Client (5cbe79c9c25524df)
full hash lists for every platform package are in Socket’s post.
sources
- Socket Research Team, “Malicious bee-js npm Release and Swarm Desktop Releases Deliver SleepyDuck Ethereum C2 Backdoor” (November 9, 2026)
- StepSecurity, “@ethersphere/bee-js 13.3.0 Compromised” (Ashish Kurmi, November 2026)
- GitHub advisory GHSA-hh7j-5m3j-23pf (issued November 9, 2026 01:25 UTC, per Socket)
- Pluto Security, EtherDuck / SleepyDuck Open VSX analysis (October 1, 2026), as cited by Socket
