skip to content

Blocked Upgrade Paths

The patch exists upstream but your direct dependency still pins the old version, so you force a transitive, exclude and redeclare, or push a fix up. Interviewers ask which one you would actually ship.

on this pageshow

questions

4

A scanner flags a vulnerable transitive dependency and a fixed version exists — why can you not always just upgrade to it?

level: juniorimportance: must knowfreq 66%

answer

  1. you did not choose that version
  2. something between you and the fix
  3. an intermediate declares the constraint
  4. patch published, graph refuses it
  5. four exits: wait, override, exclude, upstream

basics

~20 s

You do not choose that version. An intermediate library in your graph declares which version of the flagged package to pull, so until that library raises or widens its constraint, your build keeps resolving the vulnerable one.

solid answer

~50 s

The flagged package is not one I declare — it arrives because a library I do declare requires it, and that library's own constraint decides which version resolves. If it asks for an exact version, or a range that stops below the fix, adding the patched version to my manifest does not by itself change what gets installed. That is a blocked upgrade path: the patch is published, but the graph will not take it. From there I have four real exits: wait for the intermediate to release a version that allows the fix, override the resolved version myself, exclude the vulnerable one and declare the fixed version directly, or open a pull request upstream so the intermediate raises its own constraint. The first three change my build, the last one changes the actual cause. Each of the middle two buys speed at the cost of running the intermediate against a version it never tested with.

code

json · 8 lines
json
{
  "root": { "name": "payments-api", "requires": { "internal-sdk": ">=3.2,<4" } },
  "resolved": {
    "internal-sdk": { "version": "3.2.4", "requires": { "serial-lib": "==2.4.1" } },
    "serial-lib":   { "version": "2.4.1", "advisory": "fixed in 2.4.2" }
  }
  ...
}

go deeper

for a junior

Be ready to say why a transitive package is still your responsibility, and that the version installed is decided by the constraint of the library that requires it, not by your manifest alone.

for a middle

Explain the difference between an exact pin and a bounded range as blockers, and name the exits — wait, override, exclude and declare, upstream pull request — with the compatibility cost of each.

for a senior

Show that you verify remediation against the resolved graph and the built artifact, not the manifest, and that you pair any local workaround with an upstream change so it can be removed.

for a principal

Own the estate view: how many services sit behind the same blocking constraint, whether you fund an upstream contribution once instead of patching many builds, and what you commit to externally while the block stands.

## What a blocked upgrade path is Most vulnerability findings are boring to fix: an advisory names a package and a fixed version, you bump the version you declare, the finding clears. A **blocked upgrade path** is the case where that does not work. The flagged package is *transitive* — you never named it. It is present because something you did name requires it, and the version that ends up installed is chosen by that intermediate's declared constraint, not by your intent. So there are two different sentences that sound the same and are not: - "No fix exists" — the maintainer has published nothing, the project is dead, the flaw is unpatched. - "A fix exists and my graph will not take it" — the patched release is sitting in the registry, and something between you and it refuses to allow it. This question is about the second one, and the distinction matters because the responses are completely different. ## Why your own manifest is not the last word Every ecosystem resolves a graph, not a list. Your project declares a handful of direct requirements; each of those declares its own, and so on. When a package appears on more than one path, the build tool has to pick one version, and the constraints along those paths are inputs to that choice. If the only path to the flagged package runs through a library that says "I need exactly `2.4.1`", then `2.4.1` is what the resolver is being told to honour. An exact pin is the sharpest version of this — one `==` in someone else's metadata can hold an entire estate one patch release below a fix. Ranges block too, just more quietly: an upper bound like `<3` blocks a fix that first shipped in `3.0.1`, and a fix backported only to a newer minor is out of reach for anyone stuck on the old one. ## The four exits, and what each costs **1. Wait for the intermediate to release.** The cleanest outcome and often the fastest one, if a release is days away. Cost: you are carrying a known finding, and "the maintainer said soon" is not a plan you can hand an auditor without a date. **2. Override the resolved version.** Most ecosystems offer some facility to say "whatever the graph asks for, install this" — a replace directive, an overrides block, a constraints file. Cost: you are now running the intermediate library against a version of its own dependency that it never declared and never tested against. If the delta is a patch release this is usually safe; if it crosses a major version, you have swapped a security finding for a possible runtime failure, and the failure can be silent until a rarely used code path executes. **3. Exclude the vulnerable package and declare the fixed one directly.** Functionally similar to an override, but it also makes you the declaring owner of that package, which is useful — the dependency is now visible in your manifest rather than buried. Cost: the same compatibility gamble, plus a durability problem, because an exclusion is written against a coordinate and a path and both can change under you. **4. Open a pull request upstream.** The only exit that fixes the cause rather than your copy of the symptom, and the only one that helps everyone else stuck behind the same constraint. Cost: latency you do not control. These are not mutually exclusive, and the mature answer usually pairs the last one with one of the first three: ship an override now with an expiry, land the upstream change so you can delete it later. ## What a green scan actually proves After any of these, the scanner going quiet proves one thing only: the artifact it read no longer lists the vulnerable version. It does not prove the patched code is what loads. Confirm the fix by looking at the resolved graph or lockfile the build actually produced, and by scanning the built artifact rather than only the manifest — an override that failed to apply, a second path you did not exclude, or a vendored copy inside another package all produce a green manifest and a vulnerable runtime. ## The framing that reads as senior A candidate who says "just add it to the manifest" has not met a real graph. A candidate who says "we would have to fork it" has jumped past three cheaper options. The answer interviewers want names the blocker precisely — *which* dependency's *which* constraint holds the version down — and then picks an exit with its cost stated out loud.

  • Does it help that the fixed release is only one patch version above the exact pin the intermediate declares?
    It helps with compatibility, not with the block. A one-patch delta means an override is very unlikely to break the intermediate, so the technical risk of forcing it is small. But an exact pin still refuses the fix on its own, so you still have to override, exclude, or get the pin changed upstream. The small delta also makes the upstream pull request trivially easy to review, which is worth saying out loud.
  • How would you tell a blocked upgrade apart from a finding where no fix exists at all?
    Read the advisory's fixed-version field and check the registry. If a patched release is published, the problem is in my graph and the responses are override, exclude, or upstream. If nothing is published — the project is archived, or the maintainer has not shipped — then no amount of graph surgery helps and it becomes a different problem entirely. Conflating the two wastes days arguing with a resolver over a version that does not exist.
  • Your scanner reports the finding as resolved after you change your manifest. What do you check before believing it?
    The resolved graph, not the manifest. I look at the lockfile or dependency tree the build actually produced and confirm the fixed version is the only one present, then scan the built artifact rather than the source declaration. Overrides can fail to apply, a second path can reintroduce the old version, and a scanner pointed at a manifest reports intent rather than what ships.

It is like being told your building must use a newer, safer lock, while the door was fitted by a contractor whose spec sheet says one exact model. You can drill the door yourself, or get the contractor to update the spec.

saying these in an interview costs you the question

  • Says adding the fixed version to your manifest always wins
  • Assumes a green scanner means the fixed code is loaded
  • Treats a blocked upgrade as needing an immediate fork
  • Says transitive dependencies are the library author's problem
  • Confuses a blocked fix with no fix existing at all

context

open as a page

You excluded a vulnerable transitive package and declared the fixed version directly — what can silently undo that fix?

level: middleimportance: should knowfreq 41%

basics

~20 s

Upgrading the intermediate library. An exclusion is written against a specific coordinate on a specific path, so when that library changes how it pulls the package, the exclusion stops matching, the vulnerable version returns, and nothing in the build fails.

open as a page

The fix requires an intermediate library to raise its own constraint — how do you ship while that upstream pull request waits?

level: seniorimportance: should knowfreq 49%

basics

~20 s

Run two tracks. Land a local override now, scoped and tested, with an owner and a removal trigger tied to the upstream change. In parallel push the pull request itself: minimal diff, tests, the advisory cited, escalated through the project's security contact rather than a public thread.

open as a page

A vendor-managed runtime pins a vulnerable library and the vendor's fix is two quarters out — how do you decide what to do?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Establish real exposure first, then treat it as a decision that leaves engineering: isolate or move the workload if the exposure is real, otherwise accept the risk formally with a named owner, a written justification, an expiry and a review trigger — and press the vendor through the contract.

open as a page