Skip to content
HACKTECH
Guide

Written for: Engineering leads and founders running teams of five to fifty developers on JavaScript or Python

Software supply chain security for small engineering teams

What the 2025 npm and PyPI compromises teach small teams: lock installs, pin CI actions, retire long-lived tokens and review new dependencies before merge.

Published · 10 min read

A small product team ships far more code than it writes, and most of it arrives in packages nobody on the team has read. That trade is reasonable: nobody should write their own date parser. What changed in 2025 is where attackers aimed. Instead of waiting for someone to mistype a package name, they phished the maintainers of packages everyone already uses, compromised the pipelines that publish them, and wrote malware that steals the tokens needed to repeat the trick. This guide covers the controls that would have blunted those attacks, ordered so a team without a security engineer can do the cheap ones this week.

What changed in 2025

The incidents below were public and documented by the registries, by CISA or by the researchers who found them. None was aimed at one company: each went after the builds of anyone who installed the affected code.

Public supply chain incidents, December 2024 to November 2025
WhenWhat happenedThe lesson for a small team
December 2024Attackers compromised the Ultralytics project's GitHub Actions release workflow by way of its build cache and published malicious versions to PyPI, then a second round using a PyPI API token that had not been revoked.Your pipeline is a publishing credential. Keep shared caches and pull_request_target out of release workflows, and revoke tokens you no longer use.
March 2025The tj-actions/changed-files GitHub Action was compromised (CVE-2025-30066) and, between 12 and 15 March, exposed CI secrets including access keys, GitHub tokens and npm tokens. CISA said the compromise was potentially enabled by an earlier one of reviewdog/action-setup (CVE-2025-30154).A tag can be moved. Pin third-party actions to a full commit SHA.
July 2025A phishing site at pypj.org captured the credentials of four PyPI accounts, and two malicious releases of num2words were published and removed within hours.Use phishing-resistant two-factor authentication on every registry account.
8 September 2025A maintainer phished through the lookalike domain npmjs.help lost control of 18 packages, including chalk and debug, which between them see more than 2 billion downloads a week. The malicious versions redirected cryptocurrency payments in users' browsers.Old, popular packages are a target precisely because everyone installs their updates.
September 2025Malicious GitHub Actions workflows injected into many repositories sent the PyPI tokens stored in them to attackers. PyPI found no packages published with the stolen tokens and invalidated them.Long-lived publishing tokens kept as CI secrets are a standing target. Replace them with trusted publishing.
September 2025Shai-Hulud, a self-replicating worm, stole npm tokens, GitHub personal access tokens and cloud keys, published them to public repositories named Shai-Hulud, and used each victim's npm access to publish infected versions of their other packages. CISA counted more than 500 compromised packages.Install scripts run with every credential on the machine. Stop running them by default.
November 2025A second wave ran during pre-install, created more than 25,000 malicious repositories across about 350 accounts, tried to wipe the victim's home directory when theft failed, and registered infected machines as self-hosted GitHub runners.Developer laptops and CI runners are part of the supply chain. Make release runners ephemeral.

The pattern repeats. The attacker takes over a maintainer account or a pipeline, ships a malicious version through the normal update channel, runs code at install time with whatever credentials the machine holds, and uses the stolen tokens to reach the next set of packages. Each control below breaks one link in that chain.

Lock what you install

A lockfile records the exact version and integrity hash of every package in the tree, including the transitive dependencies nobody chose deliberately. Commit it, review changes to it, and make CI install from it exactly: npm ci rather than npm install, pnpm with a frozen lockfile, or pip in hash-checking mode against a fully pinned requirements file. PyPI's own advice after the Ultralytics compromise was to pin dependencies to explicit versions with checksums.

Three further habits remove much of the risk from the attacks above, because those attacks depended on fresh versions running code during installation.

  • Refuse brand-new versions. pnpm's minimumReleaseAge setting holds back any version published less than a set number of minutes ago, transitive dependencies included. With a delay of a day or two, a malicious release that is caught and pulled quickly never reaches your build: the num2words project owner deleted both malicious releases within hours of upload.
  • Do not run install scripts by default. pnpm refuses to run a dependency's build scripts until you approve that package. With npm, set ignore-scripts and run the build step explicitly for the few packages that genuinely need one.
  • Make updates deliberate. Batch routine version bumps through Dependabot or Renovate on a schedule, so every lockfile change arrives as a pull request someone reads.

Pin your pipeline and shrink what it can reach

GitHub's secure use reference warns that a compromised third-party action has access to every secret configured on your repository, and that a tag can be moved or deleted if an attacker gains access to the repository that stores the action. Pinning each action to a full-length commit SHA means the code you reviewed is the code that runs. Dependabot can still propose updates to pinned actions, so pinning does not leave you stuck on old versions.

  1. 1

    Inventory the workflows

    List every workflow, every third-party action and reusable workflow it calls, and the secrets each job can read.

  2. 2

    Pin third-party actions to commit SHAs

    Replace tags such as v4 with the full commit SHA, keep the version in a comment for readability, and let Dependabot raise the updates.

  3. 3

    Default the token to read-only

    Set GITHUB_TOKEN permissions to the minimum at the top of each workflow and grant write access only to the jobs that need it.

  4. 4

    Remove risky triggers

    Avoid pull_request_target and workflow_run for workflows that check out or run code from untrusted pull requests, and never paste pull request titles or branch names straight into shell commands.

  5. 5

    Swap stored cloud keys for OIDC

    Federate CI with AWS, Azure or Google Cloud through OpenID Connect, so no long-lived cloud credential sits in your secrets.

  6. 6

    Publish without tokens

    Move npm and PyPI releases to trusted publishing, and put the release job behind a protected environment that needs a person's approval, as PyPI recommends.

  7. 7

    Treat runners as production

    Use ephemeral runners for anything that builds releases, keep credentials off persistent self-hosted machines, and alert when a new runner registers.

Review dependencies in pull requests

A new dependency is a decision to run a stranger's code with your credentials, so it deserves the same review as any other change. GitHub's dependency review shows, inside the pull request, which packages were added, removed or updated, with their release dates, known vulnerabilities, licences and age. Its companion action fails the check when an added package carries a known vulnerability, and it is available on public repositories and on private ones with GitHub Code Security or GitHub Advanced Security.

Automation catches known vulnerabilities. It does not catch a version published an hour ago by a phished maintainer, so the reviewer's questions still matter:

  • Do we need this package, or does the standard library or an existing dependency already do the job?
  • Who maintains it, how many maintainers does it have, and when did the current version appear?
  • Does it, or anything it pulls in, run install or build scripts?
  • How many new transitive dependencies does the lockfile diff add?
  • Is the name a near miss for a popular package, or a new scope imitating a known one?
  • Does the release carry provenance linking it to a public repository and build?

Provenance, SBOMs and knowing what you ship

Provenance answers where a package came from. npm provenance links a published package to its source repository and build instructions, signs the statement with Sigstore and records it in a public transparency log, and anyone can check it with npm audit signatures. Trusted publishing, on npm and on PyPI, replaces long-lived tokens with short-lived OpenID Connect credentials issued for each build, and on supported cloud CI npm generates the provenance automatically. After Shai-Hulud, GitHub set out its plan for npm: two-factor authentication for local publishing, granular tokens limited to seven days, deprecation of legacy classic tokens and of time-based one-time passwords in favour of FIDO keys, and more providers for trusted publishing.

An SBOM answers what you shipped. CISA describes one as a nested inventory, a list of ingredients that make up software components, and put updated minimum elements out for public comment in 2025. Generate one in CycloneDX or SPDX format for every release and store it with the release artefacts; when the next advisory names a package version, you can answer in minutes. For build integrity, the SLSA framework offers a ladder: at Build L1 provenance exists, at Build L2 a hosted build platform signs it, and at Build L3 the platform is hardened so builds are isolated from one another and build steps cannot reach the signing key.

What each control proves, and what it costs a small team in effort
ControlWhat it provesEffort
Committed lockfile and exact installs in CIBuilds use the versions that were reviewedAn afternoon
Release-age delay and blocked install scriptsA fresh malicious version cannot run during installationAn afternoon, plus approving the packages that need scripts
Dependency review on pull requestsNew packages and known vulnerabilities are seen before mergeAn hour to enable, minutes per review
SHA-pinned actions and read-only tokensA moved tag or a compromised action reaches far lessAbout a day for a typical repository
Trusted publishing with provenanceReleases came from your repository and pipeline, with no token to stealHours per package
An SBOM for every releaseYou can say exactly what shipped in any releaseAbout a day to wire into CI

What an application security audit checks

An application security audit reads the code and the pipeline together, because a supply chain attack crosses both. For this part of the work we:

  • Run software composition analysis, static analysis and secrets scanning across the repository and its full history, then confirm every reported result by hand so your team does not chase noise.
  • Review dependencies for packages that are vulnerable, abandoned or over-privileged, including install scripts and lookalike names.
  • Check the lockfiles, how CI installs from them, and whether your builds produce provenance someone else can verify.
  • Follow code from merge to production: branch protection, CI permissions, third-party actions, runners, artefact signing, infrastructure as code and the secrets each stage can reach.
  • Map the gaps to NIST SSDF practices, so the result doubles as a secure development plan.

If you suspect you already installed a compromised version, treat it as an incident rather than a dependency bump. CISA's guidance after Shai-Hulud was to rotate all developer credentials immediately and to require phishing-resistant multi-factor authentication on developer accounts. Our incident response team starts triage on the first call.

An action list by effort

Start at the top; each band assumes the one above is done
WhenActionWhy
This weekTurn on phishing-resistant two-factor authentication, such as security keys or passkeys, for GitHub, npm and PyPI accountsPhishing opened the chalk, debug and num2words compromises
This weekCommit lockfiles and switch CI to exact installsBuilds stop picking up versions nobody reviewed
This weekEnable secret scanning, Dependabot security updates and branch protectionCISA recommended all three after Shai-Hulud
This monthPin third-party actions to commit SHAs and default tokens to read-onlyA moved tag or a compromised action reaches far less
This monthBlock install scripts by default and add a release-age delayBreaks the install-time execution the worms relied on
This monthAdd dependency review to pull requestsEvery new package gets a second pair of eyes
This quarterMove publishing to trusted publishing and cloud access to OIDCRemoves the long-lived tokens attackers harvest
This quarterGenerate an SBOM and provenance for every releaseYou can prove what you shipped and where it came from
This quarterMake release runners ephemeral and have the pipeline reviewed independentlyCloses the persistence route the November 2025 wave used

Most of this list is configuration rather than engineering: none of the six rows marked this week and this month touches application code. If you want someone independent to check the result, an application security audit covers the code, the dependencies and the pipeline in one engagement, a cloud configuration review covers what your pipeline can reach once it is inside AWS, Azure or Google Cloud, and secure development training builds the review habits that keep it all in place. The cloud track in our tutorials covers pipeline secrets and OIDC trust mistakes, and the Arsenal lists scanners such as Trivy and TruffleHog for dependency and secret checks.

Sources

Services that put this into practice

  • A white-box review of your application: source code, architecture, dependencies and build pipeline. Automated analysis finds the candidates; reviewers confirm each one by reading the code path, and only confirmed defects reach the report, each with a file and line reference.

    Duration: 5 to 15 working days, depending on codebase size

  • An IAM-first review of AWS, Azure or GCP. We map what an attacker reaches after compromising one workload or one developer laptop, then rank those paths by the data at the end of each and write the Terraform or policy change that cuts it.

    Duration: 4 to 8 working days

  • Hands-on workshops for engineers, run against a deliberately vulnerable copy of a codebase like yours. Developers exploit each bug before they fix it, so they have seen the attack their fix has to stop, and review real pull requests for the same bugs.

    Duration: 1 to 3 days on site or remote

  • Hands-on help when an attacker is already inside: triage on the first call, containment that does not destroy evidence, forensic scoping of what was touched, and recovery that we verify before calling it complete. Outside a crisis we run retainers, tabletop exercises and compromise assessments, so your team has practised the first hour before it happens.

    Duration: Live incidents until recovery is verified; readiness work in days

Read next

Scope the work in one call

Tell us what you ship and who is asking for assurance. A 45-minute scoping call ends with a fixed quote and a proposed start date; the written scope follows by email.