What 36 Malicious npm Packages Can Do to Your Infrastructure From a postinstall Hook

By Beatriz Costa

What 36 Malicious npm Packages Can Do to Your Infrastructure From a postinstall Hook

Between March 31 and April 4, 2026, thirty-six malicious npm packages disguised as Strapi CMS community plugins were published to the npm registry across four sock-puppet accounts.

If you installed any package matching strapi-plugin-* from the accounts umarbek1233, kekylf12, tikeqemif26, or umar_bektembiev1, remove it immediately, treat the environment as compromised, and follow the remediation steps at the end of this post. If you’re unsure whether your team has installed these packages - Safety Endpoint Security will detect these packages for you, and can be rolled out to your entire team in minutes.

Static analysis of each postinstall.js across all 36 tarballs shows the packages target Guardarian, an EU-licensed cryptocurrency payment gateway. But the techniques in these payloads are not Guardarian-specific. They are generic. If you run containers, databases, and CI/CD pipelines, these payloads demonstrate what an attacker can accomplish from inside npm install — with no user interaction beyond the install itself.

Why This Campaign Is Worth Studying

Most malicious npm packages do one thing: steal environment variables and phone home. These packages do more. Across thirty-one unique payloads (by SHA256 hash), the attacker deployed a catalog of post-exploitation techniques — all through the postinstall script hook that runs automatically on npm install.

The payloads are gated. They check the hostname: hostname === 'prod-strapi', or hostname.includes('prod'). On non-matching machines — developer laptops, unrelated CI runners — they silently exit. This means the code targets a specific environment, but it also means any analysis sandbox with the wrong hostname would never see the malicious behavior execute.

Static analysis of the source code reveals the intended function of the code, allowing us to confirm its designed behavior.

What the Payloads Are Designed to Do

Map internal infrastructure from inside a container

The nordica-recon payload (SFTY-20260403-43222 / MAL-2026-2476) resolves twenty-two internal service names from inside the Strapi container using getent hosts and dig — payment APIs, webhook handlers, admin gateways, customer-facing services.

The nordica-vhost payload (SFTY-20260403-73847 / MAL-2026-2480), analyzed before its removal from npm, enumerates thirty-six staging subdomains via Host header probing — including payment processor integrations, card services, trading APIs, and recurring payment systems.

If someone ran getent hosts against every internal hostname reachable from your CI runner or Docker container, what would they learn?

Escape the container via /proc/1/root

Five payloads attempt to read the host filesystem through /proc/1/root/. If the container runs with sufficient privileges — common in CI/CD environments and some Docker setups — this bypasses container isolation.

The advanced-uuid payload (SFTY-20260403-70957 / MAL-2026-2450) attempts to read:

  • Host SSH private keys (id_rsa, id_ed25519, authorized_keys)
  • Jenkins credentials.xml and secret.key from the host filesystem
  • Docker container configurations at /data1/app/docker/containers/*/config.v2.json
  • Git credentials (.git-credentials, .netrc)
  • All host processes via /proc/*/cmdline
  • The path /data1/app/docker/ is non-standard — it suggests knowledge of the target's specific directory layout, either from prior access or from an earlier reconnaissance payload in this same campaign.

    Spray credentials laterally

    A hardcoded PostgreSQL credential pair (user_strapi / 1QKtYPp18UsyU2ZwInVM) appears in nine payloads. Whether these credentials are valid, stale, or rotated cannot be determined from static analysis. But the pattern is revealing: the same credential is not only used for database access but also tried for:

  • SSH login to the Docker host (172.17.0.1) and two specific Hetzner IPs (SFTY-20260403-09913: guardarian-ext; SFTY-20260403-15905: nordica-deep)
  • Elasticsearch authentication at 65.21.203.242:9200, alongside target-specific passwords like elastic:guardarian and elastic:Guardarian123 (SFTY-20260403-43561: blurhash)
  • Across users, root, ubuntu, deploy, jenkins, node, strapi
  • One credential, tried against databases, SSH, and Elasticsearch across multiple hosts. How many of your infrastructure credentials are shared across services?

    Query cloud metadata

    The guardarian-ext payload (SFTY-20260403-09913 / MAL-2026-2463) queries the Hetzner Cloud metadata API — and only Hetzner. Not AWS, not GCP, not Azure:

    A separate payload (hextest, SFTY-20260403-12094 / MAL-2026-2466) targets AWS: if AWS_ACCESS_KEY_ID exists in the environment, it exfiltrates the credentials and attempts sts get-caller-identity, s3 ls, and ec2 describe-instances in eu-central-1.

    Is your cloud metadata endpoint protected from processes running inside your containers?

    Attempt CDN bypass

    Two payloads carry a hardcoded API key (a78e8684-1c99-4eb4-b899-16e55d552335) and use it to attempt direct connections to origin servers, bypassing Cloudflare. The nordica-recon payload (SFTY-20260403-43222) and nordica-cms payload (SFTY-20260403-51149) try Hetzner IPs with Host header injection:

    Whether this key is valid or rotated cannot be determined from static analysis.

    Dump the database

    The hextest payload (SFTY-20260403-12094) loads the pg Node.js module — or runs npm install pg --no-save to install it on the fly — and attempts to dump every table in the PostgreSQL database, up to 100 rows each. It also enumerates all databases on the server, all roles and permissions, and server settings.

    The seed payload (SFTY-20260403-04066 / MAL-2026-2482) uses the hardcoded credentials to probe six specific database names, including guardarian_payments, exchange, and custody.

    Mine git history

    The nordica-deep payload (SFTY-20260403-15905 / MAL-2026-2474) runs git log --all -p against the application repository, searching for deleted .env files still present in history, passwords and secrets across all commits, deploy scripts with server IPs, and git stash contents.

    If you have ever committed a secret and later removed it, this type of payload finds it.

    Establish persistence

    Several payloads include persistence mechanisms: a C2 agent written to /tmp/.node_gc.js with crontab-based restart, reverse shells spawned as detached processes, and PHP webshells written to Strapi's public uploads directory. The events payload (SFTY-20260403-29745 / MAL-2026-2460) runs a C2 polling loop that executes arbitrary commands from the server.

    The Campaign Timeline

    The publication timeline, reconstructed from the time field in each package's npm registry metadata, spans four days:

    Days 1–2 (March 31 – April 1): Twenty packages across two accounts (umar_bektembiev1, tikeqemif26). These are reconnaissance payloads — DNS enumeration, network scanning, Elasticsearch probing, host filesystem reads, SSH attempts, Cloudflare bypass tests. No reverse shells. No persistence.

    Day 3 (April 2): One package (nordica-deep). Git history mining, SSH brute-force to specific staging servers, and application code extraction.

    Day 4 (April 3): Fifteen packages across two new accounts (umarbek1233, kekylf12) using disposable email addresses. Aggressive exploitation — Redis RCE, Docker overlay escape, credential harvesting, direct database exploitation, persistent implants.

    Day 5 (April 4): One final package on a different C2 port (8888) with a different exfiltration path (/exfil2/).

    Whether the reconnaissance payloads from Days 1–2 collected the intelligence that appears hardcoded in the Day 3–4 payloads, or the attacker possessed that intelligence before the campaign began, cannot be determined from static analysis. Both interpretations are consistent with the evidence.

    What is clear: each payload reports to a unique C2 path prefix (av-, nr-, bh-, cr-, sv-, among others), allowing the operator to track which package executed on which host.

    An Observation on Tradecraft

    There is a tension in this campaign. The targeting is precise: hostname gating, hardcoded credentials, specific internal service names. But the delivery is noisy — 36 packages across 4 throwaway accounts with disposable email addresses, published over 4 days, with no obfuscation. If the attacker already had production credentials, why use a detectable delivery mechanism? If they didn't, the reconnaissance-to-exploitation pipeline they built via npm is operationally interesting and auditable.

    This observation does not lessen the significance of the findings; rather, the discrepancy between the targeted precision and the general delivery method is an area worthy of independent analysis.

    What This Means For Defenders

    The specific techniques in these payloads — cloud metadata access, /proc/1/root/ escape, credential spraying, git history mining, database dumping — are not novel individually. What this campaign demonstrates is the breadth of what a postinstall hook can accomplish when it runs inside an environment with access to infrastructure services.

    If your organization:

  • Runs npm install with network access in CI/CD or production
  • Uses Docker containers that can reach cloud metadata endpoints
  • Has Redis or PostgreSQL accessible from the application container
  • Stores secrets in .env files, environment variables, or git history
  • Has any credential been reused across services
  • Then the techniques in these payloads apply to you, regardless of whether you use Strapi.

    Indicators of Compromise

    C2 Infrastructure

  • 144[.]31[.]107[.]231:9999 — primary HTTP C2
  • 144[.]31[.]107[.]231:8888 — secondary C2
  • 144[.]31[.]107[.]231:4444 — reverse shell listener
  • C2 paths (verified in available packages): /exfil/, /exfil2/, /c2/, /db/
  • C2 paths (from removed packages): /dx/, /ir/, /rt/, /gd/, /hw/
  • Credentials Found in Malicious Source Code

    The following credentials are embedded in the publicly downloadable source code of the malicious package and have been documented in prior public reporting on this campaign. We include them so defenders can search their own environments.

  • user_strapi / 1QKtYPp18UsyU2ZwInVM — PostgreSQL credentials (9 payloads)
  • a78e8684-1c99-4eb4-b899-16e55d552335 — API key (2 payloads)
  • We cannot confirm, based on static analysis, whether these credentials are currently valid.

    Persistence Artifacts

  • /tmp/.node_gc.js
  • /tmp/vps_shell.sh, /tmp/redis_exec.sh
  • /app/public/uploads/shell.php, /app/public/uploads/revshell.js
  • /app/node_modules/.hooks.js
  • Crontab entries referencing node_gc or curl
  • npm Accounts

    Affected Packages

    Fourteen packages have been removed from npm since this analysis began, marked with †. SHA256 hashes of all tarballs are published for independent verification. OSV advisory IDs are cross-referenced where available.

  • strapi-plugin-nordica (3.6.10) — SFTY-20260403-23971 / MAL-2026-2471
  • strapi-plugin-finseven (3.6.8) — SFTY-20260403-42607 / MAL-2026-2461
  • strapi-plugin-hextest (3.6.8) — SFTY-20260403-12094 / MAL-2026-2466
  • strapi-plugin-cms-tools (3.6.8) — SFTY-20260403-45824 / MAL-2026-2453
  • strapi-plugin-content-sync † (3.6.8) — SFTY-20260403-41339 / MAL-2026-2455
  • strapi-plugin-debug-tools † (3.6.8) — SFTY-20260403-09015 / MAL-2026-2459
  • strapi-plugin-health-check † (3.6.8) — SFTY-20260403-34185 / MAL-2026-2465
  • strapi-plugin-guardarian-ext (3.6.8) — SFTY-20260403-09913 / MAL-2026-2463
  • strapi-plugin-advanced-uuid (3.6.8) — SFTY-20260403-70957 / MAL-2026-2450
  • strapi-plugin-blurhash (3.6.8) — SFTY-20260403-43561 / MAL-2026-2452
  • strapi-plugin-sitemap-gen (3.6.8) — SFTY-20260403-25961 / MAL-2026-2484
  • strapi-plugin-nordica-tools (3.6.10) — SFTY-20260403-43651 / MAL-2026-2479
  • strapi-plugin-nordica-sync † (3.6.8) — SFTY-20260403-02801 / MAL-2026-2478
  • strapi-plugin-nordica-cms (3.6.8) — SFTY-20260403-51149 / MAL-2026-2473
  • strapi-plugin-nordica-api † (3.6.8) — SFTY-20260403-85380 / MAL-2026-2472
  • strapi-plugin-nordica-recon (3.6.8) — SFTY-20260403-43222 / MAL-2026-2476
  • strapi-plugin-nordica-stage † (3.6.8) — SFTY-20260403-17222 / MAL-2026-2477
  • strapi-plugin-nordica-vhost † (3.6.8) — SFTY-20260403-73847 / MAL-2026-2480
  • strapi-plugin-nordica-deep (3.6.8) — SFTY-20260403-15905 / MAL-2026-2474
  • strapi-plugin-nordica-lite (3.6.11) — SFTY-20260403-34845 / MAL-2026-2475
  • strapi-plugin-cron (3.6.8) — SFTY-20260403-82043 / MAL-2026-2457
  • strapi-plugin-config (3.6.8) — SFTY-20260403-69173 / MAL-2026-2454
  • strapi-plugin-server (3.6.8) — SFTY-20260403-97890 / MAL-2026-2483
  • strapi-plugin-database † (3.6.8) — SFTY-20260403-71926 / MAL-2026-2458
  • strapi-plugin-core † (3.6.8) — SFTY-20260403-35381 / MAL-2026-2456
  • strapi-plugin-hooks † (3.6.8) — SFTY-20260403-08000 / MAL-2026-2467
  • strapi-plugin-monitor (3.6.8) — SFTY-20260403-34162 / MAL-2026-2470
  • strapi-plugin-events (3.6.8) — SFTY-20260403-29745 / MAL-2026-2460
  • strapi-plugin-logger (3.6.8) — SFTY-20260403-18823 / MAL-2026-2469
  • strapi-plugin-health (3.6.8) — SFTY-20260403-04554 / MAL-2026-2464
  • strapi-plugin-sync † (3.6.8) — SFTY-20260403-42797 / MAL-2026-2485
  • strapi-plugin-seed (3.6.8) — SFTY-20260403-04066 / MAL-2026-2482
  • strapi-plugin-locale † (3.6.8) — SFTY-20260403-41317 / MAL-2026-2468
  • strapi-plugin-form † (3.6.8) — SFTY-20260403-32952 / MAL-2026-2462
  • strapi-plugin-notify † (3.6.8) — SFTY-20260403-44979 / MAL-2026-2481
  • strapi-plugin-api (3.6.10) — SFTY-20260403-97632 / MAL-2026-2451
  • Remediation

    If you installed any of the affected packages:

  • Remove the package from your environment and lockfile.
  • Rotate every credential accessible from the affected environment: database passwords, API keys, JWT secrets, cloud provider keys, Kubernetes service account tokens, SSH keys, publishing tokens, git credentials, and any private keys on the filesystem.
  • Check for persistence.
  • 1. Look for `/tmp/.node_gc.js`. Audit crontab entries for `node_gc` or `curl`. 2. Kill orphaned `node -e` processes connecting to `144.31.107.231`. 3. Check Redis with `CONFIG GET dir`. 4. Inspect `/app/public/uploads/` for `shell.php` or `revshell.js`.

  • Audit your git history. Run `git log --all -p | grep -iE "password|secret|key|token|mnemonic"` and rotate anything you find.
  • Review your container security posture. Can processes running in your containers access cloud metadata endpoints? Can they read `/proc/1/root/`? Can they SSH to the Docker host?
  • Monitor network logs for outbound connections to `144[.]31[.]107[.]231` on ports `4444`, `8888`, and `9999`.
  • If you are unsure whether these packages were installed across developer machines or other endpoints, use your endpoint visibility tooling to search for the affected package names and related artifacts. Safety Endpoint Security can help identify these packages across teams and infrastructure during that exposure review.
  • This analysis is based on each `postinstall.js` payload across all 36 tarballs, combined with npm registry metadata for timeline reconstruction. All credentials and infrastructure details referenced in this post are embedded in the malicious package source code that was publicly available on the npm registry.

    Safety Firewall blocks malicious packages before they reach your environment. [Get started today.](https://www.getsafety.com/quick-start-guide)

    Read the full article