Back to core workflows Fix dependency resolution Tune package metadata Jump to monorepo patterns

Pinning Vulnerable Transitive Versions with overrides

An advisory names a package deep in your dependency tree that you never installed directly, and there is no direct upgrade to apply. This page shows how to pin the patched transitive version with overrides (or resolutions) safely.

Exact symptoms and error messages

npm audit flags a package you do not depend on directly:

Exact symptoms and error messages npm audit flags a package you do not depend on directly: Exact symptoms and error messages npm audit flags a package you do not depend on directly:
Exact symptoms and error messages — the core idea of this section at a glance.
high   Regular Expression Denial of Service in nth-check
  node_modules/svgo/node_modules/nth-check
  fix available via `npm audit fix --force`
  Will install svgo@1.3.2, which is a breaking change

Root cause analysis

The vulnerable package is pulled in transitively, so there is no direct dependency to bump. The patched version exists, but the direct parent has not released an update that references it. An override rewrites the resolved version of that specific transitive package across the graph, which is exactly what the resolution machinery in Dependency Resolution Explained supports.

Root cause analysis The vulnerable package is pulled in transitively, so there is no direct dependency to bump. Root cause analysis The vulnerable package is pulled in transitively, so there is no direct dependency to bump.
Root cause analysis — the core idea of this section at a glance.

An override is a blunt instrument, which is both its strength and its risk. It rewrites the resolved version of a package everywhere it appears in the graph, regardless of what the intermediate parents asked for. That is exactly what you want to clear a transitive advisory quickly, but it also means you are overriding the compatibility assumptions of every parent that depended on the old range — so the pinned version must genuinely satisfy all of them.

The scoping syntax lets you narrow that blast radius. A bare "nth-check": "^2.1.1" forces the version everywhere; a scoped "svgo>nth-check": "^2.1.1" forces it only under the svgo subtree. Prefer the narrow form when only one path is affected, because it leaves other consumers of the package on their originally-resolved versions and reduces the chance of a compatibility surprise elsewhere in the tree.

An advisory or a bug sometimes names a package deep in your dependency tree that you never installed directly, so there is no direct dependency to bump. The patched version exists, but the direct parent has not released an update that references it, which leaves you unable to fix the issue through a normal upgrade. An override rewrites the resolved version of that specific transitive package across the graph, which is exactly what the resolution machinery supports for this case.

The reason a forced upgrade is the wrong tool here is that it reaches across a major boundary on some direct dependency to satisfy the audit, shipping a breaking change as a side effect. An override is surgical by comparison: it pins just the transitive package to the patched version without touching your direct dependencies' majors, so remediation does not introduce unrelated breakage. The trade-off is that an override is a compatibility claim you must verify, not assume.

Resolution and configuration patch

Resolution and configuration patch Scope the override narrowly, and remove it once the parent dependency ships a release that references the patched versio Resolution and configuration patch Scope the override narrowly, and remove it once the parent dependency ships a release that references the patched version, so you do not carry a permanent pin.
Resolution and configuration patch — the core idea of this section at a glance.
// npm / pnpm
{
  "overrides": {
    "nth-check": "^2.1.1"
  }
}
// Yarn v1
{
  "resolutions": {
    "nth-check": "^2.1.1"
  }
}

Scope the override narrowly, and remove it once the parent dependency ships a release that references the patched version, so you do not carry a permanent pin.

The package managers differ in syntax, so match your tool. npm and pnpm use overrides (pnpm nests it under pnpm), while Yarn Berry uses resolutions with glob support:

// Yarn Berry — glob-scoped resolution
{
  "resolutions": {
    "svgo/**/nth-check": "^2.1.1"
  }
}

Whichever tool you use, re-run a full install so the lockfile is regenerated against the pin, then re-audit to confirm every affected path now resolves to the patched version rather than just the one you noticed.

Pin the patched transitive version with overrides (npm/pnpm) or resolutions (Yarn):

// npm / pnpm
{ "overrides": { "nth-check": "^2.1.1" } }
// Yarn Berry
{ "resolutions": { "svgo/**/nth-check": "^2.1.1" } }

Scope the override as narrowly as the fix allows — "svgo>nth-check": "^2.1.1" forces it only under svgo — and re-run install to regenerate the lockfile against the pin. Verify with npm ls nth-check that every path resolved to the patched version, then re-audit to confirm the advisory clears.

CLI validation and debug commands

CLI validation and debug commands CLI validation and debug commands in production JavaScript package workflows. CLI validation and debug commands CLI validation and debug commands in production JavaScript package workflows.
CLI validation and debug commands — the core idea of this section at a glance.
# Confirm every path now resolves to the patched version
npm ls nth-check
# Re-audit to confirm the advisory clears
npm audit --audit-level=high
# In pnpm, verify the override took effect
pnpm why nth-check

Prevention and CI guardrails

  • Prefer a narrow override over audit fix --force for transitive advisories.
  • Document why each override exists so it is removed when no longer needed.
  • Re-audit after applying so a mis-scoped override does not leave a path unpatched.
  • Let the update bot propose removing stale overrides as parents catch up.
Prevention and CI guardrails Prevention and CI guardrails in production JavaScript package workflows. Prevention and CI guardrails Prevention and CI guardrails in production JavaScript package workflows.
Prevention and CI guardrails — the core idea of this section at a glance.
  • Prefer a narrow override over audit fix --force for transitive advisories.
  • Document why each override exists so it is removed when the parent catches up.
  • Re-audit after applying so a mis-scoped override does not leave a path unpatched.
  • Let the update bot propose removing stale overrides as parents converge upstream.

Treating overrides as temporary debt

Every override is a small piece of technical debt: it pins a version that upstream has not yet caught up to, and it will silently hold that package back even after the ecosystem moves on. Left unmanaged, overrides accumulate and quietly prevent legitimate updates, so treat each one as temporary and track why it exists.

Override lifecycle Pin now, track why, remove when upstream catches up. pin patched version clear advisory document the reason tracked debt remove when upstream ready converge to ranges
An override is a temporary bridge that should be removed once upstream converges.

A one-line comment or a tracking issue per override — 'pins nth-check until svgo releases a version referencing ^2.1.1' — makes removal a deliberate follow-up rather than a forgotten pin. Let your update bot flag when the parent dependency finally references the patched version, and remove the override then. The goal is a graph that converges back to upstream ranges, with overrides as short-lived bridges, not permanent fixtures.

Verifying an override patched every path

The trap with a transitive pin is assuming it worked because the advisory you noticed cleared. A package can appear at several points in the graph, and a mis-scoped override might fix one path while leaving another on the vulnerable version. The only proof is to list every resolved copy after applying the pin.

Verify the pin List every path, confirm the patched version, re-audit. npm ls --all every path all on patched no stragglers re-audit advisory clears
Prove every path resolves to the patched version — a narrow override can miss one.
# Every path must show the patched version
npm ls nth-check --all
pnpm why nth-check
# Re-audit to confirm nothing is left behind
npm audit --audit-level=high

If ls shows even one path still on the old version, the override is too narrow — widen its scope or switch from a scoped form to a global one. Making this check part of the PR that adds the override means a partial fix is caught in review, not in the next audit run weeks later when someone assumes the issue was already handled.

Verifying an override patched every path

The trap with a transitive pin is assuming it worked because the advisory you noticed cleared. A package can appear at several points in the dependency graph, and a mis-scoped override might fix one path while leaving another on the vulnerable version. The only proof is to list every resolved copy after applying the pin and confirm they all moved.

Verifying an override patched every path The trap with a transitive pin is assuming it worked because the advisory you noticed cleared. Verifying an override patched every path The trap with a transitive pin is assuming it worked because the advisory you noticed cleared.
Verifying an override patched every path — the core idea of this section at a glance.
# Every path should show the patched version
npm ls nth-check --all
pnpm why nth-check
# Re-audit to confirm nothing is left behind
npm audit --audit-level=high

If npm ls shows even one path still on the old version, the override is too narrow — widen its scope or switch from a scoped form to a global one. Making this check part of the pull request that adds the override means a partial fix is caught in review, not in the next audit run weeks later when someone assumes the issue was already handled. Verifying every path is what turns an override from a hopeful pin into a confirmed remediation, which matters because a partially-applied security fix gives the false impression of safety while leaving a real vulnerable path in the tree.

Treating overrides as temporary debt

Every override is a small piece of technical debt: it pins a version that upstream has not yet caught up to, and it will silently hold that package back even after the ecosystem moves on. Left unmanaged, overrides accumulate and quietly prevent legitimate updates, so treating each one as temporary and tracking why it exists is what keeps the graph healthy over time.

Treating overrides as temporary debt Every override is a small piece of technical debt: it pins a version that upstream has not yet caught up to, and it will Treating overrides as temporary debt Every override is a small piece of technical debt: it pins a version that upstream has not yet caught up to, and it will silently hold that package back even af
Treating overrides as temporary debt — the core idea of this section at a glance.

A one-line comment or a tracking issue per override — 'pins nth-check until svgo releases a version referencing ^2.1.1' — makes removal a deliberate follow-up rather than a forgotten pin. Let your update bot flag when the parent dependency finally references the patched version, and remove the override then, verifying with npm ls that the graph still resolves to a safe version without the pin. The goal is a dependency graph that converges back to upstream ranges, with overrides as short-lived bridges rather than permanent fixtures. A workspace whose overrides are all documented and periodically reviewed stays close to upstream, which is both more secure and easier to update than one carrying a growing set of pins nobody remembers the reason for.

Scoped versus global overrides

An override can force a version everywhere the package appears or only under a specific dependency path, and choosing the narrowest form that resolves the problem limits the compatibility claim you are making. A global "nth-check": "^2.1.1" forces that version for every package in the graph, which is right when the vulnerable package appears on many paths that must all move; a scoped "svgo>nth-check": "^2.1.1" forces it only under svgo, leaving any other consumer of nth-check on its resolved version.

Override scope Global versus path-scoped transitive pins. Scope Forces Use when global everywhere many paths parent>dep one path one subtree default start narrow limit risk
Start scoped and widen only when the vulnerable package spans many paths.

The scoped form reduces blast radius: you assert only that svgo works with the patched version, not that the entire ecosystem does. Prefer it when only one path is affected, and reach for the global form when the vulnerable package appears on several paths that all need patching. The decision follows from the audit output, which shows every path the vulnerable package appears on — if it is one subtree, scope the override there; if it is scattered across many, a global pin is simpler and moves them all at once. Starting scoped and widening only when the paths demand it keeps overrides as precise, reviewable statements about specific compatibility rather than blunt graph-wide edicts that might mask an incompatibility elsewhere.

Frequently Asked Questions

Do overrides affect my published package's consumers?

No — overrides/resolutions apply to your own install tree, not to consumers of your package. To force a fix on consumers you must bump your own declared dependency range.

Are permanent overrides a problem?

They can mask an unmaintained parent and drift from upstream. Treat each override as temporary, document it, and remove it once the direct dependency references the patched version.

Does an override change what my package requires of its consumers?

No. overrides/resolutions apply only to your own install tree. Consumers of your published package resolve their own graph and are unaffected — to influence them you must change your declared dependency ranges.

How narrow should an override be?

As narrow as clears the issue. Use a scoped form (parent>dep) when only one path is affected so other consumers keep their resolved versions, and reserve the global form for when every path must move to the pinned version.

When should I remove an override?

Once the direct parent ships a release that references the patched version. Track each override so removal is a deliberate step; otherwise it lingers and blocks legitimate updates to that package.

How do I fix a vulnerability in a package I don't directly depend on?

Pin the patched transitive version with a scoped overrides/resolutions entry, then verify with npm ls <pkg> that every path moved to it and re-audit. This is surgical, unlike audit fix --force, which can ship an unrelated major to satisfy the advisory.

How do I confirm an override patched every path?

Run npm ls <pkg> --all or pnpm why <pkg> and check every path shows the patched version. If one path is still on the old version, the override is too narrow — widen its scope. Re-audit to confirm the advisory clears.

Do overrides affect consumers of my published package?

No — overrides/resolutions apply only to your own install tree, not to consumers of your package, who resolve their own graph. To force a fix on consumers you must change your declared dependency ranges.

Should a transitive override be scoped or global?

As narrow as clears the advisory. Use a scoped parent>dep override when the vulnerable package appears on one path, so other consumers keep their versions. Use the global form when it appears on many paths that all need patching — the audit output shows which.

Related

Dependency Auditing and Automated Updates