From package to postinstall payload: Inside the Mastra npm supply chain compromise
Mastra npm supply chain attack poisoned 140+ packages with malicious payload via account takeover.
Summary
A large-scale npm supply chain attack compromised over 140 packages within the Mastra ecosystem by taking over a maintainer account. The attacker introduced a malicious typosquat package, 'easy-day-js', which, upon installation, executed a dropper script disabling TLS verification and downloading a second-stage payload. This attack potentially exposed developer workstations and CI/CD pipelines.
Full text
Share Link copied to clipboard! Tags Malwarenpm Content types Research Products and services Microsoft Defender Topics Actionable threat insights Microsoft Threat Intelligence observed a large-scale npm supply chain attack affecting 140+ packages across the mastra and @mastra scopes on the npm registry. Microsoft shared its findings with the npm security team, and the compromised packages have been removed and the attacker’s publish access to the @mastra scope has been revoked. The compromise originated from the takeover of the ehindero npm maintainer account, which had publish rights across the Mastra ecosystem and was used to publish poisoned package versions that introduced easy-day-js, a malicious typosquat of the popular dayjs library. Once installed, easy-day-js triggered a postinstall hook that executed an obfuscated dropper script, disabled Transport Layer Security (TLS) certificate verification, contacted attacker-controlled command-and-control (C2) infrastructure, downloaded a second-stage payload, and executed the payload as a detached hidden process. The activity followed a coordinated staged delivery pattern, with a clean bait version published first, followed by a weaponized version and rapid publication of the compromised Mastra packages. Because the payload executes during installation, any developer workstation or continuous integration and continuous delivery (CI/CD) pipeline that ran npm install or npm update after the compromised versions were published was potentially exposed, regardless of whether the package was imported in application code. This created risk to credentials, tokens, build environments, and downstream software integrity. Microsoft Defender Antivirus, Microsoft Defender for Endpoint, and Microsoft Defender XDR provide detections and hunting coverage for suspicious Node.js execution, malicious package behavior, reflective code loading, persistence activity and command-and-control communication. Attack chain overview Figure 1. End-to-end attack chain from npm account takeover through mass dependency injection to second-stage payload execution. At a high level, the attack progressed through six phases: Account compromise: The attacker gained control of the ehindero npm account , a listed maintainer with publish rights across the entire @mastra scope. Typosquat creation: The attacker published easy-day-js, a package impersonating the legitimate dayjs library (57M+ weekly downloads), using a coordinating anonymous email account ). Mass poisoning: Using the compromised account, the attacker published new versions of 140+packages across the @mastra scope, each injected with easy-day-js@^1.11.21 as a new dependency. All poisoned versions were tagged as latest. Delivery: Developers and CI/CD pipelines running npm install automatically resolved to the compromised versions. The semantic versioning (SemVer) range ^1.11.21 resolved to 1.11.22, the version containing the malicious postinstall hook. Execution: The postinstall hook executed an obfuscated 4,572-byte dropper that disabled TLS verification, dropped tracking markers, and contacted the C2 server. Second-stage payload: The dropper fetched executable code from the C2 server, wrote it as a randomly named .js file, and spawned it as a fully detached, window-hidden Node.js process. Discovery and initial indicators Microsoft Threat Intelligence identified the compromise through anomalous publishing patterns on the mastra package. All previous versions of mastra (through v1.13.0) were published through GitHub Actions OpenID Connect (OIDC), the legitimate CI/CD pipeline. Version 1.13.1 was manually published by ehindero using a Tutamail address, an anonymous email service. Figure 2. Publisher comparison across mastra versions showing the anomalous manual publish on v1.13.1. The only change between mastra@1.13.0 and mastra@1.13.1 was the addition of easy-day-js@^1.11.21 as a dependency. No corresponding code changes were present in the Mastra GitHub repository. Both the compromised publisher (ehindero2016@tutamail.com) and the typosquat publisher (sergey2016@tutamail.com) used the same anonymous email provider, Tutamail. Dependency injection: the poisoned package.json The compromised mastra@1.13.1 package.json reveals the injected dependency alongside the anomalous publisher metadata: Figure 3. The compromised mastra@1.13.1 package.json with the injected easy-day-js dependency and the anomalous npm publisher. The easy-day-js dependency was not present in any prior versions of mastra npm packages. Its addition, paired with the SemVer range ^1.11.21, ensures that the npm resolves to the weaponized 1.11.22 release. Typosquat analysis: easy-day-js The easy-day-js package is a deliberate impersonation of the legitimate dayjs library: AttributeLegitimate dayjsMalicious easy-day-jsMaintaineriamkun <kunhello@outlook[.]com>sergey2016 <sergey2016@tutamail[.]com>Claimed authoriamkuniamkun (impersonated)Repository URLgithub.com/iamkun/dayjsgithub.com/iamkun/dayjs (copied)Weekly downloads57,251,792newly createdVersion count89+ versions since 20182 versions (both June 16, 2026)postinstall scriptNonenode setup.cjs –no-warnings (v1.11.22) Staged delivery pattern The typosquat used a two-phase delivery strategy: Phase 1 (clean bait): easy-day-js@1.11.21 was published at 07:05 UTC on June 16, 2026. This version contained only legitimate dayjs code with no postinstall hook. Phase 2 (weaponization): easy-day-js@1.11.22 was published at 01:01 UTC on June 17, 2026, adding the setup.cjs payload and the postinstall hook. The dayjs.min.js file is byte-identical between both versions, confirming only the dropper was added. The weaponized package.json in version 1.11.22 exposes the postinstall hook: Figure 4. The weaponized easy-day-js@1.11.22 package.json. The postinstall hook runs setup.cjs automatically on npm install. Obfuscation and payload analysis Stage 0: Obfuscated dropper (setup.cjs) The setup.cjs payload is protected with JavaScript obfuscation using rotated string arrays and a custom base64 decoder function: Figure 5. The obfuscated setup.cjs dropper with rotated string array and base64 encoded string lookups. The obfuscation technique uses a common pattern: an array of 40 Base64-encoded strings is shuffled at initialization using a numeric seed (0x4c11d), then accessed through a decoder function that performs Base64 decoding with character substitution. This prevents static analysis tools from extracting meaningful strings. Stage 1: String table decryption Decoding the rotated string array reveals the payload’s true capabilities: Figure 6. The decoded string table revealing C2 addresses, file system operations, and process spawning functionality. Key decoded strings include the secondary C2 address (23.254.164[.]123:443), Node.js built-in module references (node:child_process, node:os), and file system operations (writeFileSync, rmSync). Stage 2: Deobfuscated payload logic After resolving all string references and control flow, the full payload logic emerges as a five-step attack sequence: Figure 7. The fully deobfuscated setup.cjs payload showing the five-step attack sequence from. TLS bypass to self-deletion Step 1: Disable TLS verification. The payload sets NODE_TLS_REJECT_UNAUTHORIZED to ‘0’, disabling certificate validation for all HTTPS requests in the Node.js process. This enables communication with the C2 server without valid certificates. Step 2: Drop filesystem markers. Two tracking files are written to the OS temp directory: $TMPDIR/.pkg_history contains the install path of the compromised package, and $TMPDIR/.pkg_logs contains the package name encoded with XOR 0x80: Figure 8. XOR 0x80 decoding of the .pkg_logs marker reveals the string easy-day-js. Step 3: Fetch second-stage payload. The dropper issues a GET request to hxxps://23.254.164[.]92:8000/update/49890878 and reads the response body as text. The second-stage payload is a ~41 KB cross-platform Node.js tasking client.
Indicators of Compromise
- malware — easy-day-js
- domain — tutamail.com