Package Override Kill Switches: npm, pnpm, Yarn, Maven, Gradle & NuGet
A copy-paste reference for emergency dependency control across every major package manager — plus what to do when the only safe version of a component is one upstream no longer ships.

Package Override Kill Switches: npm, pnpm, Yarn, Maven, Gradle & NuGet
On March 31, 2026, attackers compromised the npm account of an Axios maintainer and published axios@1.14.1 and axios@0.30.4, two malicious versions that pulled in a remote access trojan through a fake plain-crypto-js dependency. The poisoned packages were live for roughly three hours before npm removed them. In that window, an estimated 600,000 installs hit production systems, CI pipelines, and developer laptops worldwide. Axios serves over 100 million weekly downloads and has 174,000 dependent npm packages, so the blast radius was effectively the entire JavaScript ecosystem.
If your team had a package override ready, you closed the door in minutes. If you didn't, you spent the next three days chasing IOCs across every Lambda, every container, and every dev workstation that ran npm install between 00:21 and 03:15 UTC.
Package manager overrides are the kill switch. They tell your build to ignore whatever the registry currently advertises and resolve to a version you trust, even when the bad version is buried four levels deep in a transitive dependency tree. Every major package manager supports this, but the syntax differs sharply between ecosystems, and most teams only learn that syntax mid-incident rather than ahead of one.
This post is a copy-paste reference for emergency dependency control across npm, pnpm, Yarn, Maven, Gradle, and NuGet, plus what to do when the only safe version of a component is one upstream no longer ships.
Why overrides are different from regular pins
A regular version pin in package.json or pom.xml only controls what your code declares directly. It does nothing about a transitive dependency three layers down that one of your direct dependencies pulled in.
An override (or resolution, or dependencyManagement entry, depending on the ecosystem) reaches past the declared graph and forces a specific version everywhere it appears, regardless of who asked for it. That is the only mechanism that closes the window during a live supply chain incident, because the compromised package is almost never something your team imported directly. It is a transitive of a transitive of a transitive.
The trade-off: overrides can break legitimate semver ranges if applied carelessly. They are emergency tools, not architectural patterns. The right workflow is to commit the override, ship it, then back it out once upstream has issued a clean release that supersedes the bad one.
npm overrides
Available in npm 8.3.0 and newer. The field lives in package.json:
{
"overrides": {
"axios": "1.14.0"
}
}
That forces every resolution of axios, direct or transitive, to 1.14.0. If you want narrower targeting, scope the override to a specific dependency path:
{
"overrides": {
"some-package": {
"axios": "1.14.0"
}
}
}
You can also key the override on a version specifier so it only applies when the bad version is requested:
{
"overrides": {
"axios@1.14.1": "1.14.0"
}
}
After editing package.json, regenerate the lockfile with npm install --package-lock-only, then use npm ci in CI. Skipping the lockfile regeneration is the most common reason overrides quietly fail to apply. For more on npm options, check out our guide to npm overrides.
pnpm overrides
pnpm uses a dedicated pnpm.overrides block inside package.json:
{
"pnpm": {
"overrides": {
"axios": "1.14.0",
"axios@<1.14.0": "1.14.0"
}
}
}
pnpm also supports parent-scoped overrides using the > delimiter:
{
"pnpm": {
"overrides": {
"foo>axios": "1.14.0"
}
}
}
That applies only when axios is pulled in by foo. Useful when one consumer needs the patched version and the rest of the tree should follow normal resolution.
After editing, run pnpm install. pnpm rewrites pnpm-lock.yaml in place. In CI, use pnpm install --frozen-lockfile to fail loudly if the lockfile and overrides disagree.
Yarn resolutions
Yarn (Classic and Berry) uses a resolutions block in package.json:
{
"resolutions": {
"axios": "1.14.0",
"**/axios": "1.14.0"
}
}
The **/axios glob applies the override anywhere in the dependency tree. The unqualified axios entry covers the top-level case. Including both is belt-and-suspenders for incident response.
Yarn Berry (2+) supports more granular descriptor-level overrides:
{
"resolutions": {
"axios@npm:^1.14.0": "1.14.0"
}
}
After editing, run yarn install. Yarn rewrites yarn.lock. In CI, use yarn install --frozen-lockfile (Classic) or yarn install --immutable (Berry).
Maven dependencyManagement
Maven has no single "overrides" field. Emergency control comes from three primitives that work together.
1. Force a transitive version with dependencyManagement in pom.xml:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
</dependencyManagement>
Anything in the build that pulls in jackson-databind, no matter how deep, now resolves to 2.17.2. Maven's "nearest-wins" rule is overridden by dependencyManagement.
2. Exclude a bad transitive entirely:
<dependency>
<groupId>com.example</groupId>
<artifactId>some-lib</artifactId>
<version>3.4.0</version>
<exclusions>
<exclusion>
<groupId>org.compromised</groupId>
<artifactId>bad-pkg</artifactId>
</exclusion>
</exclusions>
</dependency>
Use this when you need to surgically drop a transitive without replacing it. Often paired with a dependencyManagement entry that pins the safe replacement.
3. Pin an entire stack with a BOM import:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
BOMs are the cleanest way to lock dozens of related artifacts in one statement, which is critical during incidents that span an entire ecosystem (Spring fan-out, Jackson, log4j-style cascades).
Run mvn dependency:tree -Dverbose=true after editing to confirm the override applied. Maven silently falls back to the declared version if your override is malformed, so always verify.
Gradle force, constraints, and strictly
Gradle gives you three tools, increasing in precision.
1. force on the resolution strategy (Groovy DSL, build.gradle):
configurations.all {
resolutionStrategy {
force 'com.fasterxml.jackson.core:jackson-databind:2.17.2'
}
}
Force is the bluntest instrument. It overrides every other resolution rule, which is exactly what you want during an active incident.
2. Dependency constraints with strictly (Gradle 4.6+):
dependencies {
constraints {
implementation('org.apache.logging.log4j:log4j-core') {
version {
strictly '2.17.1'
}
because 'CVE-2021-44228 and CVE-2021-45046'
}
}
}
Constraints let you set a version without declaring a dependency on the artifact yourself. The strictly qualifier rejects any other version, even if a transitive demands it. The because field is documentation that survives in build scans.
3. Kotlin DSL equivalent (build.gradle.kts):
configurations.all {
resolutionStrategy {
force("com.fasterxml.jackson.core:jackson-databind:2.17.2")
}
}
Verify with ./gradlew dependencies --configuration runtimeClasspath. Look for the (c) marker (constraint) or the version arrow showing the upgrade was actually applied.
NuGet Central Package Management and VersionOverride
NuGet has historically been the hardest ecosystem to control during a supply chain incident, because the default resolution rule ("lowest applicable version") often pulls in a transitive that no project file declares. As of NuGet 6.2 and .NET SDK 6.0.300, Central Package Management (CPM) makes emergency overrides clean.
1. Pin a transitive across an entire solution using Directory.Packages.props:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.2" />
</ItemGroup>
</Project>
Drop Directory.Packages.props at the repository root. Every .csproj under it inherits these versions, and CentralPackageTransitivePinningEnabled extends the override past direct references into the transitive graph. This is the closest .NET equivalent to Maven's dependencyManagement import or Gradle's force.
2. Per-project carve-out with VersionOverride:
<ItemGroup>
<PackageReference Include="System.Text.Json" VersionOverride="8.0.6" />
</ItemGroup>
Use this when one project needs a different version than the central file dictates. Disable it solution-wide with <CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled> if you want the central file to be the only source of truth.
3. Emergency fallback without CPM (legacy .csproj and packages.config):
<ItemGroup>
<PackageReference Include="System.Security.Cryptography.Xml" Version="8.0.2" />
</ItemGroup>
If CPM is not enabled, the fastest in-incident fix is to add a direct PackageReference to the vulnerable transitive in every affected project. NuGet's "direct-dependency-wins" rule means the declared version takes precedence over anything pulled in transitively. The trade-off is sprawl: you now own a dependency you did not previously declare, and you need to remove it once upstream ships a clean release.
Run dotnet list package --include-transitive and dotnet nuget why <PackageId> to verify the override resolved as expected. NuGet's dotnet list package --vulnerable --include-transitive command also cross-references your graph against the GitHub Advisory Database, which is the fastest way to confirm you closed the gap.
A current example: CVE-2026-26171, the EncryptedXml denial-of-service vulnerability disclosed on April 14, 2026 Patch Tuesday, affected System.Security.Cryptography.Xml across .NET 6, 8, 9, and 10. .NET 8, 9, and 10 received upstream patches the same day. The kill-switch pattern for a still-supported version was a one-line PackageVersion update in Directory.Packages.props pointing at the patched package. .NET 6 received no upstream fix, which is the scenario the next section covers.
The first-hour playbook
When the next Axios-class incident hits, the sequence is:
- Identify the safe version. Usually the last release before the malicious one. For Axios that was 1.14.0.
- Add the override to every affected package.json, pom.xml, build.gradle, or Directory.Packages.props in your monorepo or repo set.
- Regenerate lockfiles in a single commit. Do not let dev environments and CI drift.
- Verify with the package manager's dependency tree command that the override applied transitively, not just at the top level.
- Block the bad version in your private registry. Artifactory, Nexus, GitHub Packages, JFrog, and Verdaccio all support excluding specific versions or version ranges. This is your defense in depth: even if a future commit drops the override, the registry refuses to serve the bad artifact.
- Roll back the override once upstream has shipped a clean release that supersedes the malicious one.
Steps 1 through 4 should take under an hour for a team that has practiced. Most teams discover during the incident that they have never practiced.
When the only safe version is already EOL
Overrides assume a safe version exists. That assumption breaks in two scenarios:
- A CVE is disclosed in a framework whose supported branch has reached end of life. There is no patched version to override to, because the maintainers have stopped shipping. CVE-2026-26171 in .NET 6 is the canonical recent example: Microsoft confirmed the vulnerability, declined to issue a patch, and left .NET 6 users with no upstream version to point a PackageVersion at.
- The "safe" version is so old that pinning to it breaks half of your other dependencies (Spring 4.x, AngularJS 1.x, Node.js 18, Node.js 20 after April 30, 2026, .NET 6, .NET 8 and 9 after November 10, 2026, Rails 6, Tomcat 8.5).
This is where override mechanics run out. You cannot pin your way out of "upstream is gone." You can accept the unpatched exposure, freeze the environment and hope nothing else breaks, or move to a maintained fork.
That last option is the gap HeroDevs Never-Ending Support (NES) closes. NES ships patched, drop-in replacements for end-of-life frameworks across npm (AngularJS, Vue 2, Node.js), Maven (Spring 4/5/6, Spring Boot, Tomcat), NuGet (.NET 6 today, .NET 8 and 9 after November 10, 2026), PyPI, RubyGems, and others. Maven artifacts publish under com.herodevs.nes group IDs, the npm packages keep the same names with a HeroDevs-controlled version line, and the NuGet packages drop into Directory.Packages.props the same way any other PackageVersion entry does. The override patterns above work exactly the same way. Your dependencyManagement block (or overrides, or resolutions, or PackageVersion) points at a HeroDevs version, your scanners stop alerting, and your SOC 2, PCI DSS, or CRA audit posture stays intact while the team plans the real migration.
For teams that have been waiting for the right pattern: HeroDevs is the version you override to when upstream is no longer an option.
Taking action
Run a five-minute check today, before the next incident:
- Open your most critical package.json, pom.xml, build.gradle, or Directory.Packages.props. Confirm you (or whoever is on-call) can add an override and regenerate the lockfile in under five minutes.
- Confirm your private registry is configured to block specific versions and ranges. If you do not have a private registry in front of npm, Maven Central, or NuGet, that is the higher-priority gap.
- Inventory your end-of-life components with EOLDS, the free EOL dependency scanner from HeroDevs. Anything flagged as EOL is a component where overrides will not save you in the next incident.
If you are sitting on EOL frameworks where a kill switch is no longer an option, talk to HeroDevs. The patched versions exist, and the override syntax you just learned is exactly how you wire them in.


