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:
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.
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
// 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
# 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 --forcefor 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.
- Prefer a narrow override over
audit fix --forcefor 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.
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.
# 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.
# 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.
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.
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 — the overview for auditing and update automation.