Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Securing npm, PyPI and Go dependencies

By updated

Improve this page

A dependency is third-party code that runs with the rights of your application, and sometimes as soon as it is installed. This lesson shows how to close the three doors attackers use most, typosquatting, dependency confusion and install scripts, then how to pin and verify what you install from npm, PyPI and Go modules. It is for a developer or a DevOps engineer who manages dependencies every day. The commands were run on 1 October 2026 with npm 10.9.9, pip-audit 2.9.0, Go 1.25 and cosign 3.1.3.

What you will learn

  • Recognise typosquatting, dependency confusion and malicious install scripts
  • Disable npm scripts and inspect those of a package before allowing them
  • Pin dependencies with a lockfile, npm ci and pip hashes
  • Protect internal packages with an npm scope and a single index
  • Verify the provenance of an npm package, a PyPI package or a signed binary
  • Link these measures to requirements SOCLE-FRN-OSS-3 (in French) and SOCLE-INT-OSS-2 (in French) of the SOCLE framework

The attack vectors

Three techniques come up in most dependency compromises. They do not target your code but your package manager, and they rely on its default behaviours: install what it is asked for, choose a version, run the scripts a package declares.

Typosquatting

Typosquatting exploits typing mistakes. The attacker publishes a package whose name looks like that of a popular package, and waits for a developer to make a mistake when typing the command.

Legitimate packageTyposquatDifference
lodashlodahsSwapped letters
requestsrequestSingular and plural
tensorflowtensorfowMissing letter
@angular/coreangular-coreDifferent format

The scenario is mundane: you want requests, the most widely used Python HTTP client, and you type one letter too few. The malicious package installs without error, often bundles the real code of the imitated package so as not to raise suspicion, and runs at installation a script that exfiltrates the credentials present on the workstation. PyPI and npm regularly remove such packages, but some stay online for several days before being detected.

Terminal window
# One letter less, another package
pip install request # ❌ similar name, different author
pip install requests # the expected package

Dependency confusion

Dependency confusion targets a company's internal packages. The attacker discovers their name, through a leaked package.json, an error message or published code, then publishes on the public registry a package with the same name, in a very high version such as 99.0.0. If the package manager can find that name on the public registry, it may prefer the highest version. The following diagram shows this mechanism with pip, and the countermeasure that neutralises it.

Dependency confusion with pip: the attacker publishes acme-utils 99.0.0 on public PyPI, pip configured with extra-index-url queries both PyPI and the private index that holds acme-utils 1.2.0, keeps the highest version and installs 99.0.0 on the build machine; with a single index, the private proxy, the internal version 1.2.0 is installed.highest versionfixAttacker: publishesacme-utils 99.0.0Public PyPIPrivate index:acme-utils 1.2.0pip withextra-index-url99.0.0 installedon the build machineFix: a single index,the private proxyInternal 1.2.0installed
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

The risk depends on the ecosystem, and it is the point that guides simplify most often:

SituationRisk
pip with --extra-index-urlHigh: pip queries every index and keeps the highest version
npm, internal name without a scope (acme-utils)High: the default registry is the public registry
Proxy registry that merges public and private (virtual repository)High: the proxy may serve the highest public version
npm, scoped name (@acme/utils) associated with a private registryLow: a scope resolves on a single registry
npm, scope not reserved on npmjs.comHigh: the attacker can create the @acme organisation and publish in it

In 2021, researcher Alex Birsan demonstrated the attack against Microsoft, Apple and PayPal by publishing packages named after their internal dependencies: his code ran on their build machines, and he received more than 130,000 dollars in bounties. The countermeasures, detailed by ecosystem below, come down to two measures: a reserved npm scope associated with the private registry, and a single pip index.

Malicious install scripts

Package managers can run code at installation time. It is useful for compiling a native module, and it is also the favourite entry point for malware: the code runs even before you have imported the package.

ManagerMechanismRuns code at installation?
npmpreinstall, install, postinstallYes, by default
pipbuilding a source distribution (setup.py, build backend)Yes, if no wheel is available
GononeNo

npm protection

npm is the most targeted ecosystem, because of its size and its install scripts. The protections below go from the simplest to the most robust, and they add up.

Disabling install scripts

A package can declare scripts that npm runs during npm install. Measured: esbuild 0.24.2 declares a postinstall (node install.js), and npm install runs it. With ignore-scripts=true, the same npm install runs no script at all and installs the 72 packages of the test project.

.npmrc
# No package script is run at installation
ignore-scripts=true
Terminal window
# Or occasionally, without a configuration file
npm install --ignore-scripts

Before allowing a package that really needs its scripts, such as a native module, read what they do. npm view displays the declared scripts without installing anything; an earlier version of this lesson suggested npm run-script --dry-run, which only lists the scripts of your project and tells you nothing about a third-party package.

Terminal window
npm view esbuild@0.24.2 scripts
# { postinstall: 'node install.js' }

Checking a package before installing it with npq

npq steps in before npm install and checks the requested package against several signals: package known to be malicious, presence of scripts, age, popularity, maintainers. Measured on the lodahs typosquat, it blocks the installation and quotes the database that flags it:

Terminal window
npx npq@3 install lodahs --dry-run
Packages with issues found:
--- Package: lodahs@latest ---
ERROR: Supply Chain Security - Malicious package found: https://snyk.io/vuln/npm:lodahs
Summary:
- Total packages: 1
- Total errors: 1
- Total warnings: 0

Pinning exact versions

By default, npm records version ranges (^4.17.0 means "4.17.0 or newer, without changing the major version"). If a malicious 4.18.0 version is published, the next npm install without a lockfile takes it. Measured: the save-exact option is false by default; set it to true so that npm records the exact version.

Terminal window
npm config set save-exact true
NotationMeaningRisk
"4.17.21"Exactly this versionUnder control
"^4.17.0"From 4.17.0 to 4.x.xAutomatic minor updates
"~4.17.0"From 4.17.0 to 4.17.xAutomatic patches
"*"Any version❌ To be banned

Mandatory lockfile, and npm ci in CI

The package-lock.json lockfile pins the exact version and the checksum (integrity) of each dependency, direct or transitive. It is always committed. In CI, npm ci applies it strictly where npm install may rewrite it. Measured: without a lockfile, npm ci refuses to run (EUSAGE); with a lockfile that no longer matches package.json, it also refuses, instead of adapting it silently.

Terminal window
npm ci
# npm error code EUSAGE
# npm error `npm ci` can only install packages when your package.json and
# package-lock.json or npm-shrinkwrap.json are in sync.
CommandBehaviourUse
npm installMay update dependencies and the lockfileDevelopment workstation
npm ciInstalls exactly the lockfile, fails if it divergesCI/CD, production

Private registry for internal packages

Against dependency confusion, two measures complement each other. Give your internal packages a scope (@acme/…) and associate it with your private registry in .npmrc: npm will never look for an @acme/* package anywhere else. Then reserve the @acme organisation on npmjs.com, even empty, so that no third party can publish in it.

.npmrc
# All @acme/* packages come ONLY from the private registry
@acme:registry=https://npm.acme-corp.example/
# Everything else comes from the public registry
registry=https://registry.npmjs.org/

If your private registry is a proxy that aggregates the public registry, check that it never serves the public version of an internal name: this kind of virtual repository is how confusion comes back.

PyPI protection

PyPI now requires two-factor authentication from all its maintainers and offers token-free publishing through Trusted Publishers. It remains exposed to typosquatting, and pip runs code when it has to build a package from its sources.

Auditing vulnerabilities with pip-audit

pip-audit resolves the dependencies of a requirements.txt and checks them against vulnerability databases (OSV and PyPI's advisory database). Measured on a file that only contains requests==2.25.1: it finds 13 vulnerabilities in 3 packages, requests and its dependencies idna and urllib3, and exits with code 1, which is enough to fail a CI job.

Terminal window
pip install pip-audit==2.9.0
pip-audit -r requirements.txt
Found 13 known vulnerabilities in 3 packages
Name Version ID Fix Versions
-------- ------- ------------------- ------------
requests 2.25.1 PYSEC-2023-74 2.31.0
requests 2.25.1 PYSEC-2026-1873 2.32.0

Avoiding code execution at installation

pip runs code when it builds a package from a source distribution (.tar.gz). A wheel (.whl) is already built: installing it runs no code. The --only-binary :all: option forbids source distributions.

Terminal window
pip install --only-binary :all: requests

The trade-off is measurable: pyyaml==5.3.1 has no wheel for Python 3.12, and pip answers No matching distribution found. This refusal is intended; it flags a package to audit or to replace with a version that publishes wheels.

Package typeExtensionRuns code at installation?
Source distribution.tar.gzYes (build)
Wheel.whlNo

A single index for internal packages

With extra-index-url, pip queries every index and keeps the highest version: a 99.0.0 published on PyPI wins over your internal 1.2.0. The countermeasure is to declare only one index, your private registry, which acts as a proxy to PyPI for public packages and reserves the internal names.

pip.conf
[global]
# A single index: the private proxy, which also serves public packages
index-url = https://pypi.acme-corp.example/simple/

The file goes in ~/.config/pip/pip.conf on Linux, ~/Library/Application Support/pip/pip.conf on macOS and %APPDATA%\pip\pip.ini on Windows.

Pinning with a hash

Pinning a version does not protect against a file replaced under the same number. The SHA-256 hash of the file, added to the line, does: pip refuses to install a file whose checksum differs. Measured: the hash of the requests-2.31.0-py3-none-any.whl wheel is indeed the one below, and a wrong hash makes the installation fail with Expected sha256 … Got ….

requirements.txt
requests==2.31.0 \
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f

pip-compile, from pip-tools, generates these lines for all dependencies, direct and transitive, from a requirements.in that only lists your direct dependencies.

Terminal window
pip install pip-tools
pip-compile --generate-hashes requirements.in

Go modules protection

Go was designed with the supply chain in mind. It runs no code when a module is installed, and verifies by default each module against a public checksum database. Measured on Go 1.25: GOPROXY is https://proxy.golang.org,direct and GOSUMDB is sum.golang.org.

RisknpmpipGo
Scripts at installationYes, by defaultYes, for a source distributionNo
Dependency checksumsIn the lockfileOptional (--hash)go.sum, mandatory
Public checksum databaseNoNosum.golang.org

Verifying module integrity

The go.sum file contains the checksum of each module, and go mod verify checks that the local cache matches them. Measured: on an intact cache, the command answers all modules verified; after adding a line to a file of the github.com/google/uuid module, it answers dir has been modified and exits with code 1.

Terminal window
go mod verify
# all modules verified
# After tampering with the cache
# github.com/google/uuid v1.6.0: dir has been modified (/go/pkg/mod/github.com/google/uuid@v1.6.0)

A tampered cache is purged and downloaded again, and go.sum is always committed:

Terminal window
go clean -modcache
go mod download

Private modules: GOPRIVATE

By default, Go asks the public proxy for each module and verifies its checksum against sum.golang.org: an internal module cannot be found there, and its request reveals its name. The GOPRIVATE variable lists the prefixes of private modules, which Go asks neither the proxy nor the checksum database for (documentation of go help private).

Terminal window
export GOPRIVATE=github.com/acme-corp/*,gitlab.acme.example/*
# With a private proxy (Athens, Artifactory), combine the two
export GOPROXY=https://goproxy.acme.example,https://proxy.golang.org,direct

Verifying the provenance of dependencies

The previous protections reduce the attack surface, but do not answer one question: was this package built from the source code that can be audited? In the xz-utils backdoor, in 2024, the script that activated the malicious payload was only present in the release archives, not in the Git repository: rereading the repository's code was not enough to see it.

A provenance answers this question. It is a signed attestation that links the artifact to its repository, its commit and the platform that built it. The SLSA (in French) framework (Supply-chain Levels for Software Artifacts) defines its levels of requirement, and the in-toto (in French) format describes its attestations.

npm: registry signatures and provenance

npm distinguishes two proofs. Every version on the public registry carries a registry signature, which proves that the package has not been modified since it was published. The provenance, for its part, only exists if the maintainer published with npm publish --provenance from a compatible CI. npm install verifies neither: it is npm audit signatures that does.

Terminal window
npm audit signatures
audited 72 packages in 0s
72 packages have verified registry signatures
1 package has a verified attestation

Measured on the test project (express, lodash, esbuild): all 72 packages have a valid registry signature, but only one carries a provenance attestation. Provenance remains a minority: use it as a selection criterion for your critical dependencies, not as a general blocking condition.

PyPI: Trusted Publishers and attestations

Trusted Publishers, introduced by PyPI in 2023, let a project publish from an identified CI workflow, with a short-lived OIDC token, without a stored API token. Be careful about what they guarantee: they do not prevent a maintainer from also publishing with an API token as long as one exists. Since late 2024, PyPI also exposes attestations for files published this way, which can be queried through its API.

Terminal window
curl -s -H 'Accept: application/vnd.pypi.integrity.v1+json' \
https://pypi.org/integrity/pydantic/2.11.7/pydantic-2.11.7-py3-none-any.whl/provenance

Measured: pydantic 2.11.7 and sampleproject 4.0.0 return an attestation that names the repository and the workflow used for publishing (pydantic/pydantic, ci.yml). requests 2.32.4 returns 404: this very popular package publishes no attestation. So never assume that a well-known package is published by a Trusted Publisher: check it.

Binaries: verifying a Sigstore signature

A binary downloaded from a release page is verified with its signature. The most common format is the Sigstore bundle (.sigstore.json), verified with cosign. Measured on cosign 3.1.3, which signs its own binaries:

Terminal window
# 1. Download the binary and its bundle
gh release download v3.1.3 --repo sigstore/cosign \
--pattern 'cosign-linux-amd64' --pattern 'cosign-linux-amd64.sigstore.json'
# 2. Verify the signature AND the signer's identity
cosign verify-blob cosign-linux-amd64 \
--bundle cosign-linux-amd64.sigstore.json \
--certificate-identity keyless@projectsigstore.iam.gserviceaccount.com \
--certificate-oidc-issuer https://accounts.google.com
# Verified OK

The --certificate-identity option is decisive: without it, any valid Sigstore signature would pass, and you would have verified the cryptography without verifying the origin. The certificate of the measured bundle does carry the identity keyless@projectsigstore.iam.gserviceaccount.com. If verification fails, do not run the binary.

Choosing a dependency

Adding a dependency means trusting code and people you do not know. A few criteria can be checked in a few minutes, before adding it, and avoid abandoned dependencies or poorly maintained ones.

CriterionHow to checkBenchmark
OpenSSF Scorecard scorePublic API or scorecard --repo=…Above 5 out of 10
Latest releasePackage pageLess than 6 months
Active maintainersRepository contributorsAt least 2
Provenancenpm audit signatures, PyPI integrity APIPresent if possible
Responsiveness to flawsRepository security advisoriesFixes in weeks, not months

The public Scorecard API saves you from installing the tool. Measured on 1 October 2026 for expressjs/express: score 8.9 out of 10 (Scorecard v5.5.0), with Code-Review, Maintained and SAST at 10. Two checks are -1, that is, not assessable, not zero: Signed-Releases for lack of a GitHub release, and Branch-Protection for lack of sufficient rights to read it. A score is therefore read check by check, not only overall.

Terminal window
curl -s https://api.securityscorecards.dev/projects/github.com/expressjs/express \
| jq '{score, date, checks: [.checks[] | {name, score}]}'

The OpenSSF Scorecard guide details the tool and its integration in CI.

Universal best practices

These six rules sum up the lesson and apply to the three ecosystems. They are put in place in this order, from the quickest to the most structural.

  1. Commit the lockfile: package-lock.json, go.sum, requirements.txt with hashes.

  2. Install strictly in CI: npm ci, pip install --require-hashes, go mod verify.

  3. Audit regularly: npm audit, pip-audit, and an SCA scanner in CI.

  4. Isolate internal packages: reserved npm scope, single pip index, GOPRIVATE.

  5. Disable install scripts, and allow them case by case after reading them.

  6. Verify the provenance of critical dependencies before adopting them.

Detection tools

The tools below complement the package managers. The first two were measured in this lesson; the next ones widen coverage to the OSV database or to the detection of suspicious behaviour in the packages' code.

ToolEcosystemDetects
npqnpmKnown malicious packages, scripts, weak signals
pip-auditPyPIKnown vulnerabilities
OSV-Scanner (in French)MultiVulnerabilities and malicious packages listed in OSV
Socketnpm, PyPISuspicious behaviour in the packages' code
SnykMultiVulnerabilities and licences

Key points

  1. Typosquatting exploits a typing mistake; npq blocks known malicious packages.
  2. Dependency confusion hits pip with extra-index-url and npm without a reserved scope.
  3. ignore-scripts=true cuts off npm scripts; npm view <package> scripts shows them beforehand.
  4. The lockfile is committed, and npm ci refuses a missing or out-of-sync lockfile.
  5. A pip hash refuses a replaced file; go mod verify detects a tampered cache.
  6. npm install verifies nothing: npm audit signatures checks signatures and provenance, which is still rare.
  7. A signature is verified with the signer's identity, never without it.
  8. Evidence expected by SOCLE-FRN-OSS-3 and INT-OSS-2: locked versions and a list of authorised registries.

Knowledge check

Check that you have the essentials of this lesson. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

External resources

These reference documentations go deeper into each ecosystem.

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn