{"slug": "how-typescript-devs-can-avoid-getting-pwned-by-malicious-packages", "title": "How TypeScript devs can avoid getting pwned by malicious packages", "summary": "TypeScript developers face increased risk from malicious npm packages due to coding agents enabling faster vulnerability exploitation. Recent attacks like Shai-Hulud 2.0 and Mini Shai-Hulud compromised thousands of packages by abusing lifecycle scripts and release pipelines. Developers can mitigate threats by configuring release-age cooldowns and blocking build/lifecycle scripts in package managers like pnpm, npm, and Bun.", "body_md": "[back](/writing)\n\n# How TypeScript devs can avoid getting pwned by malicious packages\n\nThird-party dependencies have always been an attack vector for malicious actors.\n\nBut coding agents have made the problem much, much worse. Between the volume of code being generated and the agents’ abilities to find vulnerabilities, it has gotten much easier to put malicious code inside of npm packages.\n\nLast November, the Shai-Hulud 2.0 attack showed just how quickly a compromised package could affect the entire npm ecosystem. The attack used a self-replicating npm worm that backdoored 796 packages totaling more than 20 million weekly downloads. It ran malicious code through package lifecycle scripts, allowing it to spread across developer machines and CI/CD pipelines as soon as someone installed an affected package.[1](#user-content-fn-1)\n\nAnd 2026 hasn’t been much better. A related attack, called Mini Shai-Hulud, harvested tokens and credentials from CI/CD runners and developer machines. By abusing legitimate publishing infrastructure, attackers were able to push compromised package versions through trusted release paths.\n\nIt first impacted the SAP developer ecosystem in late April. 2 Then on May 11, it compromised the TanStack release pipeline and published 84 malicious versions across 42\n\n`@tanstack/*`\n\npackages.\n\n[3](#user-content-fn-3)## Three config changes that do the heavy lifting\n\nThe following three controls address most of the common npm supply-chain attack paths with relatively little setup.\n\n### Use a release-age cooldown\n\nA package version’s release age is the amount of time that has passed since it was published. A release-age policy tells your package manager to install only versions that have been available for a minimum amount of time.\n\nThis is a very effective security measure because malicious versions are often discovered quickly. Security researchers and automated scanners identify them, registries remove them, and maintainers publish clean replacements, usually in a matter of hours or days. By avoiding brand-new releases, you give that process time to happen before a poisoned version reaches your machine or CI pipeline.\n\nHow long you should wait depends on your risk tolerance. Seven days is a reasonable baseline. For more sensitive projects, consider increasing the window to 14 days.\n\nHere is how to configure it in each package manager:\n\n-\n**pnpm:** Set`minimumReleaseAge`\n\nin`pnpm-workspace.yaml`\n\n. The value is measured in minutes, so`10080`\n\nrepresents seven days. In pnpm v11, the default is`1440`\n\n, or 24 hours. Use`minimumReleaseAgeExclude`\n\nto exempt specific packages from the requirement. -\n**npm:** Set`min-release-age`\n\nin`.npmrc`\n\n. The value is measured in days, so`7`\n\nrepresents one week. It is disabled by default. Use`min-release-age-exclude`\n\nto exempt specific packages or package-name patterns. -\n**Bun:** Set`minimumReleaseAge`\n\nunder`[install]`\n\nin`bunfig.toml`\n\n. The value is measured in seconds, so`604800`\n\nrepresents seven days. The exclusion setting is the plural`minimumReleaseAgeExcludes`\n\n.\n\n### Block build and lifecycle scripts\n\nnpm packages can declare lifecycle scripts, such as `preinstall`\n\nand `postinstall`\n\n, that run automatically during installation.\n\nThese scripts are dangerous because developers generally don’t review them before installing a package. They can execute arbitrary code as soon as installation begins, potentially reading environment variables, SSH keys, npm tokens, cloud credentials, and other secrets before you’ve run any of your own code.\n\nLifecycle scripts have been one of the most common execution mechanisms in recent npm supply-chain attacks. The Shai-Hulud campaigns used installation-time execution to compromise machines, steal credentials, and spread to additional packages.\n\nThe safest approach is to deny dependency install scripts by default and explicitly permit only the packages that genuinely need them.\n\n-\n**pnpm:** Dependency build scripts are blocked unless explicitly permitted. In pnpm v11, configure approved and denied packages using the`allowBuilds`\n\nmap in`pnpm-workspace.yaml`\n\n. Avoid globally enabling all build scripts unless you fully trust every dependency in the project. -\n**npm:** For a blanket block, set`ignore-scripts=true`\n\nin`.npmrc`\n\n. It defaults to`false`\n\n, meaning npm normally runs dependency lifecycle scripts during installation. Current npm 11 releases also support package-level decisions through the`allowScripts`\n\npolicy and the`npm approve-scripts`\n\nand`npm deny-scripts`\n\ncommands. -\n**Bun:** Bun blocks dependency lifecycle scripts by default and includes a built-in allowlist for some commonly used packages. Add additional packages through`trustedDependencies`\n\nin`package.json`\n\n. One thing to watch out for, setting`trustedDependencies`\n\nin your config replaces Bun’s built-in list rather than extending it, so you must re-add any packages from that list that you still want to trust.\n\nNot every install script can be blocked, and that’s fine. Packages such as `sharp`\n\n, `esbuild`\n\n, Prisma, and Playwright may require installation-time setup.\n\nThe goal is to understand which packages require these scripts and why. That awareness lets you explicitly decide which scripts to permit and which code may run on your machine.\n\n### Block exotic transitive dependencies\n\nWhen you install a package from the npm registry, that package can declare its own dependencies using sources outside the registry, including Git repositories, remote tarballs, and local file paths.\n\nThese sources can bypass some of the checks applied by the registry, and they are often harder to audit and reproduce. Even if you’ve carefully vetted your direct dependencies, a dependency of a dependency could still pull code through one of these alternative sources.\n\nBlocking exotic transitive dependencies closes that door.\n\nIf you genuinely need a dependency sourced from Git or a tarball, add it as a direct dependency and review it explicitly. The goal is to prevent transitive dependencies from unexpectedly sourcing code from outside the npm registry.\n\n-\n**pnpm:** Set`blockExoticSubdeps: true`\n\nin`pnpm-workspace.yaml`\n\n. -\n**npm:** In npm 11.15.0 and later, these sources are controlled through`allow-git`\n\n,`allow-remote`\n\n,`allow-file`\n\n, and`allow-directory`\n\n. All four settings default to`all`\n\n, so you must set them to`none`\n\nyourself if you want to block those sources.[4](#user-content-fn-5) -\n**Bun:** Bun does not currently provide a first-party equivalent. The fallback is to review your lockfile and enforce a policy requiring explicit approval for Git or tarball dependencies.\n\n## Better, but not perfect\n\nThe three controls above will substantially reduce your exposure, but they aren’t a complete defense.\n\nThe biggest gap is malicious runtime code. Blocking install scripts stops code from running during installation, but it doesn’t stop a package’s normal application code from being backdoored. If malicious behavior is hidden inside a function you’re legitimately importing and calling, none of the three controls above will catch it.\n\nAnother limitation is that a release-age cooldown assumes malicious versions will be identified quickly. A patient or well-hidden attacker may evade the protection simply by remaining undetected longer than your configured cooldown.\n\nThese controls also assume you’re installing the package you intended to install. They don’t prevent typosquatting or dependency-confusion attacks, where a typo or naming trick causes you to install a malicious lookalike package.\n\nHere are a few additional protections worth layering on:\n\n-\n**Commit your lockfile and install from it.** Use`npm ci`\n\n,`pnpm install --frozen-lockfile`\n\n, or`bun install --frozen-lockfile`\n\nin CI so that you install the exact versions you’ve reviewed. -\n**Run a dependency scanner.** Tools such as Socket, Snyk,`npm audit`\n\n, and Dependabot can flag known malicious packages and vulnerabilities that get past the controls above. -\n**Keep your dependency count low.** Every package you add increases your attack surface. The fewer dependencies you have, the fewer opportunities there are for something to go wrong. -\n**Review updates instead of automatically merging them.** Be especially careful with changes that affect your build system, deployment process, or CI configuration. -\n**Review lockfile changes.** An unexpected Git URL, tarball source, lifecycle script, or large transitive dependency tree should be treated as a warning sign. -\n**Limit credentials available during installation.** Avoid exposing production secrets, broad npm tokens, or unnecessary cloud credentials to routine dependency-install jobs.\n\n## Conclusion\n\nThird-party dependencies aren’t going anywhere, and neither are the people trying to sneak malicious code into them. AI has tilted the field in attackers’ favor. It is faster and easier than ever to identify weaknesses, compromise packages and publishing infrastructure, and distribute malicious code to projects with millions of weekly downloads.\n\nThe good news is that many attacks rely on a small set of recurring mechanisms, and you can block several of the most common attack paths without much effort.\n\nWait before installing newly released versions. Block dependency install scripts by default. Don’t allow transitive dependencies to source code from unexpected places. Commit and enforce your lockfile.\n\nIf you take away only one thing, make it this: consider switching to pnpm v11. Most of the protections described here are already enabled by default, giving you a safer baseline with very little configuration.\n\nWhatever package manager you choose, remember that dependency security is an ongoing habit, not something you can set once and forget.\n\n## Footnotes\n\n-\nMicrosoft Security.\n\n[“Shai-Hulud 2.0: Guidance for Detecting, Investigating, and Defending Against the Supply Chain Attack.”](https://www.microsoft.com/en-us/security/blog/2025/12/09/shai-hulud-2-0-guidance-for-detecting-investigating-and-defending-against-the-supply-chain-attack/)December 9, 2025.[↩](#user-content-fnref-1) -\nSnyk.\n\n[“A Mini Shai-Hulud Has Appeared: Bun-Based Stealer Hits SAP @cap-js and mbt npm Packages.”](https://snyk.io/blog/bun-based-stealer-hits-sap-cap-js-mbt-npm-packages/)April 29, 2026.[↩](#user-content-fnref-2) -\nSnyk.\n\n[“TanStack npm Packages Compromised Inside the Mini Shai-Hulud Supply Chain Attack.”](https://snyk.io/blog/tanstack-npm-packages-compromised/)May 11, 2026.[↩](#user-content-fnref-3) -\nnpm.\n\n[“npm CLI Changelog: Version 11.15.0.”](https://docs.npmjs.com/cli/v11/using-npm/changelog/)May 20, 2026.[↩](#user-content-fnref-5)", "url": "https://wpnews.pro/news/how-typescript-devs-can-avoid-getting-pwned-by-malicious-packages", "canonical_source": "https://builtbystef.com/blog/supply-chain-security/", "published_at": "2026-07-16 04:43:43+00:00", "updated_at": "2026-07-16 04:56:44.045384+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-safety"], "entities": ["npm", "pnpm", "Bun", "TanStack", "SAP", "Shai-Hulud"], "alternates": {"html": "https://wpnews.pro/news/how-typescript-devs-can-avoid-getting-pwned-by-malicious-packages", "markdown": "https://wpnews.pro/news/how-typescript-devs-can-avoid-getting-pwned-by-malicious-packages.md", "text": "https://wpnews.pro/news/how-typescript-devs-can-avoid-getting-pwned-by-malicious-packages.txt", "jsonld": "https://wpnews.pro/news/how-typescript-devs-can-avoid-getting-pwned-by-malicious-packages.jsonld"}}