TRENDING
A mountain gorilla looks at the camera from green forest undergrowth
October 29, 2026
How to Compress Time-Series Metrics in Python With Gorilla Delta-of-Delta and XOR Encoding
A bronze-framed hourglass with white sand beside a small Ferris wheel ornament on a wooden table against a teal wall
October 29, 2026
NIST’s Final Post-Quantum Transition Report Puts Every Quantum-Vulnerable Public-Key Scheme on One 2030 and 2035 Clock, and Three Federal Documents Word the Deadline Three Ways
Six weathered stone columns of the Temple of Apollo at Delphi, Greece, standing on a stone platform above a valley of olive groves
October 29, 2026
Honeypots Recorded Probes Against Oracle E-Business Suite CVE-2026-101767 Seven Days After Its Patch, and the Finder’s Write-Up Is Dated Patch Day
Vintage silver stopwatch with a chain on a black background, its dial showing seconds from 5 to 60
October 29, 2026
How to Find a Slow Query in PostgreSQL 18 With pg_stat_statements, Read Its EXPLAIN ANALYZE Plan, and Fix It With the Right Index
A wooden Tower of Hanoi puzzle on a market table, with three pegs and stacked discs of different sizes
October 29, 2026
Two Labs’ Claimed Refutations of the Exponential Time Hypothesis Turn Verification Into a Statement-Reading Problem, and Their Lean Theorems Promise Only That a Constant Exists
29 Oct 2026
SXZ.io SXZ.io
  • Home
Search the Site
Popular Searches:
Technology Amazon AI
Recent Posts
A flock of snow geese in flight, seen as dark silhouettes against an orange and red sunset sky with layered clouds
How to Run Zero-Downtime Schema Migrations on PostgreSQL 18 and Block Unsafe ALTER TABLE Statements With a Python Lint Gate
October 28, 2026
An old green steel truss bridge resting on temporary supports beside a new arch bridge under completion on a river in Portland, Oregon
The Angular 2 Rewrite Turns a Migration Lesson Into a Security Bill: AngularJS Still Draws 533,480 Downloads a Week, and 10 Advisories Have No Open-Source Fix
October 28, 2026
Hundreds of yellow rubber ducks floating in a river enclosure during a duck race
WordPress 7.1.4 Closes an Unauthenticated Race in Its Own Update Routine, but 19 Older Branches Had No Backport Hours After Release
October 28, 2026
SXZ.io SXZ.io
  • Home

Categories

Articles 270 Posts
News 273 Posts
Learning Hub 243 Posts
Home/Learning Hub/How to Set Up Dependency Cooldowns in npm and pip to Block Fresh Malicious Packages
Learning Hub

How to Set Up Dependency Cooldowns in npm and pip to Block Fresh Malicious Packages

Learn to configure npm and pip dependency cooldowns so installs only trust package versions old enough to have survived the supply chain attack detection window.

July 30, 2026 11 Min Read
186

A malicious npm or PyPI package usually does not stay malicious-looking for long. Attackers publish a poisoned release, automated build pipelines around the world pull it within minutes, and only later does someone notice the strange network calls or the stolen credentials and get the package pulled from the registry. By then the damage is already sitting in production. A dependency cooldown is a simple, config-only defense against exactly that timeline: you tell npm or pip to refuse any package version that has not been publicly available for some minimum amount of time, so the version reaching your machine has already survived the window where most malicious releases get caught and removed. This tutorial teaches you how to turn that on for both npm and pip, watch it actually block a fresh release, and safely override it when you genuinely need a package published five minutes ago.

Table Of Content

  • Prerequisites
  • How a Dependency Cooldown Actually Works
  • Step 1: Confirm Your Toolchain Is New Enough
  • Step 2: Understand Where These Settings Can Live
  • npm’s three locations
  • pip’s three locations, and the default that surprises people
  • Step 3: Set Up an npm Cooldown
  • Create a project and set the cooldown
  • Watch it block a real, currently fresh release
  • Override it for a genuine emergency
  • The sibling option: an exact date instead of a day count
  • Step 4: Set Up a pip Cooldown
  • Create an isolated virtual environment and set the cooldown
  • Watch it block a real, currently fresh release
  • Override it for a genuine emergency
  • Step 5: Add Vulnerability Scanning as a Second Layer
  • Common Mistakes and Gotchas
  • Verifying Everything End to End
  • Next Steps

This build was done on Ubuntu 26.04 LTS with Node.js v24.18.0 and npm 11.16.0 for the npm half, and Python 3.11.15 with pip 26.2 for the pip half. Every command below, and every block and error message shown, was run for real against the live npm and PyPI registries while writing this tutorial, not copied from documentation.

Prerequisites

  • A Linux machine (this tutorial uses Ubuntu; the commands are the same on any distribution, only installation of Node.js and Python differs).
  • npm 11.10.0 or later. The min-release-age setting used in this tutorial does not exist in older npm releases. Run npm --version to check; if you are behind, install a current Node.js LTS from nodejs.org or via your package manager.
  • pip 26.1 or later. The --uploaded-prior-to flag used in this tutorial only accepts a plain duration (like P1D for one day) from pip 26.1 onward; pip 26.0 requires a full ISO 8601 datetime instead. This is a property of pip itself, not of your Python version, so any Python 3 interpreter works as long as its pip is current. Upgrade with python3 -m pip install --upgrade pip.
  • Comfort running npm install and pip install already. This tutorial explains the cooldown mechanism in depth but assumes you know what a package manager does.
  • No root or sudo is required for the recommended project-scoped and virtual-environment-scoped setup used here. Root is only needed if you choose the machine-wide (global) scope covered later.

How a Dependency Cooldown Actually Works

Both npm and PyPI record an upload timestamp for every version of every package. A cooldown setting does not scan for malware or compare hashes against a blocklist; it does something much simpler and, because of that, much harder for an attacker to route around. When you run an install, the package manager computes a cutoff time (either “N days before right now” or an exact date you supply), then builds your dependency tree using only versions uploaded before that cutoff. A version published an hour ago simply does not exist as far as the resolver is concerned, in exactly the same way a version that was never published does not exist. If nothing satisfies your version requirement before the cutoff, the install fails loudly instead of silently grabbing the newest thing.

This matters because of how fast most supply-chain incidents get caught. Registries, security researchers, and automated scanners routinely spot and pull malicious packages within hours to a few days of publication. A cooldown of even 24 to 72 hours means your installs only ever see versions that have already survived that scrutiny window. It will not stop every attack, and it does nothing for a compromise that stays hidden longer than your cooldown, but it closes off the most common and most damaging pattern: a burst of automated installs in the first few minutes after a malicious publish.

Step 1: Confirm Your Toolchain Is New Enough

Check both version numbers before doing anything else, since an older npm or pip will simply reject the settings below as unrecognized:

node --version
npm --version
python3 --version
pip --version

This tutorial’s environment reported:

v24.18.0
11.16.0
Python 3.11.15
pip 26.2 from /path/to/venv/lib/python3.11/site-packages/pip (python 3.11)

Both comfortably clear the 11.10.0 and 26.1 minimums. If pip --version comes back lower than 26.1, run python3 -m pip install --upgrade pip first; everything past this point assumes the newer flag syntax.

Step 2: Understand Where These Settings Can Live

Before setting anything, it helps to know that both tools support the same basic idea: a setting can live at a scope narrower than “this whole machine, forever.” Getting the scope wrong is the single most common mistake with this feature, so it is worth teaching explicitly rather than skipping to the command.

npm’s three locations

npm’s config set command takes a --location flag with three values:

  • project, writes to a .npmrc file in the current directory. Only affects installs run from that project.
  • user, writes to your personal ~/.npmrc. Applies to every project you run as that user, unless a project-level file overrides it.
  • global, writes to npm’s global config file, typically requiring root. Applies machine-wide.

pip’s three locations, and the default that surprises people

pip’s config set command takes --global, --user, or --site flags with a similar meaning, --site being the per-virtual-environment equivalent of npm’s project scope. The gotcha: if you run pip config set without passing any of those three flags, pip does not default to the current virtual environment. Running pip config debug shows the real precedence order pip searches, and the documentation is explicit that a bare pip config set falls through to the user-level file whenever a virtual environment’s own config file does not already exist yet, which it usually does not on a fresh environment. In practice that means a bare pip config set global.uploaded-prior-to P1D run inside a fresh virtual environment quietly writes to your personal ~/.config/pip/pip.conf instead, a file that will keep affecting every other virtual environment and system-wide install you ever run as that user, not just the project you meant to scope it to. Always pass --site, --user, or --global explicitly and never rely on the default.

For learning purposes, this tutorial uses project scope for npm and --site scope for pip: both are isolated to one folder, both are safe to experiment with, and both are the closest match to how most people actually want this to behave, contained to a specific project rather than silently changing every install on the machine.

Step 3: Set Up an npm Cooldown

Create a project and set the cooldown

mkdir cooldown-demo && cd cooldown-demo
npm init -y
npm config set min-release-age 2 --location=project

This writes a single line to a new .npmrc file in the current directory:

min-release-age=2

min-release-age takes a whole number of days. Setting it to 2 means npm will only install versions that were published more than two days ago.

Watch it block a real, currently fresh release

npm itself ships new versions of the npm package constantly, which makes it a convenient, always-available way to see the cooldown do something real instead of a contrived example. At the time of writing, npm 11.19.0 had been published under a day earlier. With the two-day cooldown above active, installing it directly fails:

npm install [email protected]
npm error code ETARGET
npm error notarget No matching version found for [email protected] with a date before 7/28/2026, 2:53:56 AM.
npm error notarget In most cases you or one of your dependencies are requesting a package version that doesn't exist.

That is the cooldown working exactly as intended. npm computed “two days before right now,” found that version 11.19.0 was published after that cutoff, and refused to consider it at all, the same error you would get if the version genuinely did not exist.

Override it for a genuine emergency

Sometimes the version you need really was published an hour ago, for example a hotfix for a bug that is actively breaking your build. Rather than turning the whole cooldown off, override it for a single install with a CLI flag:

npm install [email protected] --min-release-age=0
added 1 package, and audited 145 packages in 2s

Setting --min-release-age=0 on the command line for that one invocation takes precedence over the project-level .npmrc, so the install proceeds normally. The persistent setting in .npmrc is untouched, so the very next plain npm install goes back to enforcing the two-day window. Confirm that by removing the package and reinstalling an older, already-aged version with no override at all:

rm -rf node_modules package-lock.json
npm install [email protected]

npm 11.18.0 was published about a month before this tutorial was written, well outside the two-day window, so it installs immediately with no error and no override needed, exactly the everyday case a cooldown is designed to leave alone.

The sibling option: an exact date instead of a day count

npm also has a before option that takes an exact date instead of a relative day count: npm install --before=2026-07-01 would only allow versions published on or before July 1, 2026. The two are meant to work together rather than compete: if both apply to the same source (say, both are set in the same .npmrc), the exact date in before wins over the relative min-release-age window, which makes sense since an explicit date is a more specific instruction than a rolling window. before is most useful for reproducing a build as it existed on a specific past date rather than for ongoing supply-chain protection, which is what min-release-age is for.

One npm detail worth being precise about: some published npm documentation references a companion min-release-age-exclude setting for exempting specific package names from the cooldown. Testing directly against the npm 11.16.0 CLI used in this tutorial, that setting does not exist; passing it returns min-release-age-exclude is not a valid npm option. If you need to install one specific package without a cooldown, use the --min-release-age=0 per-install override shown above, or check npm config list -l against your own installed npm version before relying on an exclude list.

Step 4: Set Up a pip Cooldown

Create an isolated virtual environment and set the cooldown

mkdir cooldown-demo-pip && cd cooldown-demo-pip
python3 -m venv venv
source venv/bin/activate
pip config --site set global.uploaded-prior-to P1D

Two things to notice here. First, --site is passed explicitly, for the reason covered in Step 2, writing the setting into this virtual environment’s own pip.conf rather than your personal user config. Second, the key is global.uploaded-prior-to: the global here is a section name inside pip’s config file meaning “apply to every pip subcommand,” not the --global scope flag; pip’s config file sections are named after the command they apply to (install, list, and so on), and global is the special section that applies everywhere. The value P1D is an ISO 8601 duration meaning one day.

This writes to venv/pip.conf:

[global]
uploaded-prior-to = P1D

Watch it block a real, currently fresh release

pip itself is a convenient live target for the same reason npm was: version 26.2 of the pip package on PyPI had been published under a day before this was written. With the one-day cooldown active:

pip install "pip==26.2"
ERROR: Could not find a version that satisfies the requirement pip==26.2 (from versions: 0.2, 0.2.1, [... dozens of older releases omitted here for length ...], 26.0, 26.0.1, 26.1, 26.1.1, 26.1.2)
ERROR: No matching distribution found for pip==26.2

The bracketed note above is this tutorial’s own editing, added only to shorten a version list going back to pip’s very first 2008 release; the real output lists every single one. What matters is what is missing from that list: it stops at 26.1.2, the last release older than the one-day cutoff. 26.2 is not merely filtered out of the list, it is treated as if it does not exist at all, matching npm’s behavior. This filtering only works against indexes that publish upload-time metadata alongside each release; PyPI’s own index does, so the default case works out of the box, but a private or mirrored index (Artifactory, Nexus, devpi, and similar) needs to be checked, since one that does not forward that metadata will silently let the cooldown do nothing rather than error.

Override it for a genuine emergency

pip install "pip==26.2" --uploaded-prior-to=P0D
Successfully installed pip-26.2

P0D means a zero-day minimum age, effectively “no cooldown, just for this one install.” As with npm, this only overrides the single command it is attached to; the persistent venv/pip.conf setting is unchanged, so the next plain pip install in this environment enforces the one-day window again.

Step 5: Add Vulnerability Scanning as a Second Layer

A cooldown protects you from installing something too recently published to have been vetted yet. It does nothing for a vulnerability that was always there, sitting in a package that has been stable and trusted for years. That is a different problem, solved by a different kind of tool: a vulnerability scanner that checks your actual installed dependency tree against a known-issues database.

npm has this built in:

npm audit

pip does not ship an equivalent, but the community-maintained pip-audit tool fills the same role. Install and run it inside your virtual environment:

pip install pip-audit
pip-audit

Running it in this tutorial’s own throwaway environment turned up a real, specific finding, useful as a sample of what the tool’s output looks like:

Found 2 known vulnerabilities in 1 package
Name       Version ID              Fix Versions
---------- ------- --------------- ------------
setuptools 79.0.1  PYSEC-2026-3447 83.0.0

A cooldown and an audit tool are complementary, not redundant: the cooldown buys time for the community to find problems in brand-new releases, and the audit tool catches problems that were already known about releases you may have installed long ago.

Common Mistakes and Gotchas

  • Relying on pip’s default scope. As covered in Step 2, a bare pip config set with no --site, --user, or --global flag writes to your personal user config far more often than people expect, quietly affecting every other project you touch. Always pass the scope flag explicitly.
  • Assuming min-release-age-exclude exists. It is referenced in some npm documentation but does not work as an actual config key in the npm 11.16.0 CLI tested for this tutorial; use the --min-release-age=0 per-install override instead.
  • Setting the cooldown, then having no documented override process. A cooldown that blocks a real production hotfix at 2 AM with no one on the team knowing the override flag is worse than not having one, since it turns a security control into an outage. Document --min-release-age=0 and --uploaded-prior-to=P0D next to wherever your team keeps its incident runbooks.
  • Assuming it works against any registry. Both mechanisms depend on the index serving upload-time metadata. Public npm and public PyPI do. An internal mirror might not, and the failure mode is silent (the cooldown simply does not filter anything) rather than an error, so it is worth testing against your actual internal registry, not just the public one, before trusting it in production.
  • Forgetting this also delays your own team’s packages. If your organization publishes internal packages through the same registry, a machine-wide cooldown delays your own releases from reaching your own machines by the same window. That is sometimes desirable (it catches your own mistakes too) and sometimes not; decide on purpose rather than by accident.

Verifying Everything End to End

Confirm both settings are actually active before trusting them:

npm config get min-release-age --location=project
pip config list

The npm command should print 2 (or whatever number of days you chose), and pip config list should show global.uploaded-prior-to='P1D' sourced from your virtual environment’s pip.conf. Then repeat the core test from Steps 3 and 4 whenever you want to sanity-check a live environment: try installing a package version you know was published within your cooldown window and confirm it is refused, then add the override flag and confirm it succeeds. If both halves of that behave as expected, the cooldown is genuinely protecting installs from that environment, not just sitting inert in a config file.

Next Steps

A dependency cooldown is one layer in a broader supply-chain defense, not a complete one. From here, consider pairing it with a lockfile that pins exact, already-vetted versions so a cooldown is only ever consulted when you deliberately bump a dependency; this site’s walkthrough of pip’s pylock.toml format covers that for Python. For a container-based deployment, generating and signing a software bill of materials closes a related gap; see this site’s guide to SBOMs with Syft and Cosign. Whichever you add next, the goal is the same one this tutorial started with: make it structurally harder for a compromised dependency to reach a machine that matters before someone has had a chance to notice.

Tags:

Node.jsnpmpipPythonSupply Chain Security

Share

Close-up of colorful network and power cables tangled behind a rack of servers, with blue and green status LEDs
Previous Post

Kubernetes’ controller-runtime Cache Shows Why Controller Reads Never Touch the API Server

Three hardware security keys used for two-factor authentication
Next Post

Okta Agrees to Acquire Permiso Security for Almost $200 Million

No Comment! Be the first one.

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Latest
28 Oct
How to Run Zero-Downtime Schema Migrations on PostgreSQL 18 and Block Unsafe ALTER TABLE Statements With a Python Lint Gate
28 Oct
The Angular 2 Rewrite Turns a Migration Lesson Into a Security Bill: AngularJS Still Draws 533,480 Downloads a Week, and 10 Advisories Have No Open-Source Fix
Trending
October 28, 2026
How to Run Zero-Downtime Schema Migrations on PostgreSQL 18 and Block Unsafe ALTER TABLE Statements With a Python Lint Gate
October 28, 2026
The Angular 2 Rewrite Turns a Migration Lesson Into a Security Bill: AngularJS Still Draws 533,480 Downloads a Week, and 10 Advisories Have No Open-Source Fix
October 28, 2026
WordPress 7.1.4 Closes an Unauthenticated Race in Its Own Update Routine, but 19 Older Branches Had No Backport Hours After Release
October 28, 2026
How to Compress Time-Series Metrics in Python With Gorilla Delta-of-Delta and XOR Encoding
October 28, 2026
NIST’s Final Post-Quantum Transition Report Puts Every Quantum-Vulnerable Public-Key Scheme on One 2030 and 2035 Clock, and Three Federal Documents Word the Deadline Three Ways
October 28, 2026
Honeypots Recorded Probes Against Oracle E-Business Suite CVE-2026-101767 Seven Days After Its Patch, and the Finder’s Write-Up Is Dated Patch Day

Related Posts

A laptop wrapped in a chain and padlock, illustrating least-privilege controls for AI agents.
Learning Hub

How to Secure Tool-Using AI Agents Before They Touch Production

June 8, 2026
Colorful sticky notes arranged on an office wall, symbolizing governance checklists and planning.
Learning Hub

AI Governance for Agentic Apps: A Practical Checklist for Builders

June 8, 2026
A technician connects green fiber optic cables at a data center, representing a private production inference endpoint.
Learning Hub

How to Deploy a Fine-Tuned LLM Behind a Private Production Inference Endpoint

June 8, 2026
Narrow aisle behind black supercomputer racks in a data center
Learning Hub

Kubernetes SELinux Volume Labeling: What Cluster Operators Should Audit Before v1.37

June 8, 2026
SXZ.io SXZ.io
  • [email protected]

Categories

Articles
Learning Hub
News

All Rights Reserved by SXZ.io ©2026