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 ciand 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 package | Typosquat | Difference |
|---|---|---|
lodash | lodahs | Swapped letters |
requests | request | Singular and plural |
tensorflow | tensorfow | Missing letter |
@angular/core | angular-core | Different 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.
# One letter less, another packagepip install request # ❌ similar name, different authorpip install requests # the expected packageDependency 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.
The risk depends on the ecosystem, and it is the point that guides simplify most often:
| Situation | Risk |
|---|---|
pip with --extra-index-url | High: 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 registry | Low: a scope resolves on a single registry |
| npm, scope not reserved on npmjs.com | High: 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.
| Manager | Mechanism | Runs code at installation? |
|---|---|---|
| npm | preinstall, install, postinstall | Yes, by default |
| pip | building a source distribution (setup.py, build backend) | Yes, if no wheel is available |
| Go | none | No |
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.
# No package script is run at installationignore-scripts=true# Or occasionally, without a configuration filenpm install --ignore-scriptsBefore 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.
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:
npx npq@3 install lodahs --dry-runPackages 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: 0Pinning 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.
npm config set save-exact true| Notation | Meaning | Risk |
|---|---|---|
"4.17.21" | Exactly this version | Under control |
"^4.17.0" | From 4.17.0 to 4.x.x | Automatic minor updates |
"~4.17.0" | From 4.17.0 to 4.17.x | Automatic 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.
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.| Command | Behaviour | Use |
|---|---|---|
npm install | May update dependencies and the lockfile | Development workstation |
npm ci | Installs exactly the lockfile, fails if it diverges | CI/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.
# All @acme/* packages come ONLY from the private registry@acme:registry=https://npm.acme-corp.example/
# Everything else comes from the public registryregistry=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.
pip install pip-audit==2.9.0pip-audit -r requirements.txtFound 13 known vulnerabilities in 3 packagesName Version ID Fix Versions-------- ------- ------------------- ------------requests 2.25.1 PYSEC-2023-74 2.31.0requests 2.25.1 PYSEC-2026-1873 2.32.0Avoiding 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.
pip install --only-binary :all: requestsThe 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 type | Extension | Runs code at installation? |
|---|---|---|
| Source distribution | .tar.gz | Yes (build) |
| Wheel | .whl | No |
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.
[global]# A single index: the private proxy, which also serves public packagesindex-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 ….
requests==2.31.0 \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003fpip-compile, from pip-tools, generates these lines for all
dependencies, direct and transitive, from a requirements.in that only
lists your direct dependencies.
pip install pip-toolspip-compile --generate-hashes requirements.inGo 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.
| Risk | npm | pip | Go |
|---|---|---|---|
| Scripts at installation | Yes, by default | Yes, for a source distribution | No |
| Dependency checksums | In the lockfile | Optional (--hash) | go.sum, mandatory |
| Public checksum database | No | No | sum.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.
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:
go clean -modcachego mod downloadPrivate 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).
export GOPRIVATE=github.com/acme-corp/*,gitlab.acme.example/*
# With a private proxy (Athens, Artifactory), combine the twoexport GOPROXY=https://goproxy.acme.example,https://proxy.golang.org,directVerifying 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.
npm audit signaturesaudited 72 packages in 0s
72 packages have verified registry signatures
1 package has a verified attestationMeasured 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.
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/provenanceMeasured: 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:
# 1. Download the binary and its bundlegh 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 identitycosign 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 OKThe --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.
| Criterion | How to check | Benchmark |
|---|---|---|
| OpenSSF Scorecard score | Public API or scorecard --repo=… | Above 5 out of 10 |
| Latest release | Package page | Less than 6 months |
| Active maintainers | Repository contributors | At least 2 |
| Provenance | npm audit signatures, PyPI integrity API | Present if possible |
| Responsiveness to flaws | Repository security advisories | Fixes 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.
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.
-
Commit the lockfile:
package-lock.json,go.sum,requirements.txtwith hashes. -
Install strictly in CI:
npm ci,pip install --require-hashes,go mod verify. -
Audit regularly:
npm audit,pip-audit, and an SCA scanner in CI. -
Isolate internal packages: reserved npm scope, single pip index,
GOPRIVATE. -
Disable install scripts, and allow them case by case after reading them.
-
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.
| Tool | Ecosystem | Detects |
|---|---|---|
| npq | npm | Known malicious packages, scripts, weak signals |
| pip-audit | PyPI | Known vulnerabilities |
| OSV-Scanner (in French) | Multi | Vulnerabilities and malicious packages listed in OSV |
| Socket | npm, PyPI | Suspicious behaviour in the packages' code |
| Snyk | Multi | Vulnerabilities and licences |
Key points
- Typosquatting exploits a typing mistake; npq blocks known malicious packages.
- Dependency confusion hits pip with
extra-index-urland npm without a reserved scope. ignore-scripts=truecuts off npm scripts;npm view <package> scriptsshows them beforehand.- The lockfile is committed, and
npm cirefuses a missing or out-of-sync lockfile. - A pip hash refuses a replaced file;
go mod verifydetects a tampered cache. npm installverifies nothing:npm audit signatureschecks signatures and provenance, which is still rare.- A signature is verified with the signer's identity, never without it.
- 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
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
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: OpenSSF Scorecard
A new full quiz with random questions
Work again only on the questions you missed
External resources
These reference documentations go deeper into each ecosystem.