Closing Renovate's transitive-CVE blind spot
Renovate's vulnerability alerts only cover direct dependencies in npm and pip. Here is the CI-gate and manual-remediation pattern that catches the rest, and why I run Renovate without Dependabot.
The previous post in this series hardened the Renovate config: OSV scanning, release-age cooldowns, SHA pin protection. A security bump for torch still broke uv lock last week, and fixing that surfaced four independent vulnerabilities Renovate had never opened a PR for. The gap is structural, not a config mistake.
Failure chain
A Renovate PR bumped torch from 2.11.0 to 2.12.1 to fix a CVE. CI failed uv lock --check with an unsatisfiable dependency set:
torchvision==0.26.0 depends on torch==2.11.0
your-project[all] depends on torch==2.12.1
→ requirements are unsatisfiable
torch, torchvision, and torchaudio ship as a matched set with mutual exact-version pins. Renovate saw them as three independent packages and bumped one.
Fixing the matched set unblocked the lockfile, and the next CI step (pip-audit) failed on three more CVEs in jupyter-server, jupyterlab, and msgpack. None of these are direct dependencies. pip-audit queries OSV live and catches the full installed tree, so it caught them. Renovate had stayed silent.
The frontend gate failed next: pnpm audit flagged three high-severity CVEs in undici, pulled in transitively through jsdom. Same pattern. The audit gate saw the full tree; the update bot saw only direct dependencies.
The root cause is documented but easy to miss. Renovate exposes two vulnerability mechanisms, and both are direct-only in practice1:
osvVulnerabilityAlertsqueries the OSV database locally via osv-offline. Direct dependencies only, for both npm and pip.vulnerabilityAlertsconsumes GitHub advisory data and opens fix PRs. Also direct-only in practice.
A transitive vulnerability produces zero automated PRs. The CI audit gates catch it; a human fixes it.
Remediation
Four fixes, one per failure, plus two config hardenings.
Matched-set ecosystems
torch, torchvision, and torchaudio must move together. I added a pytorch group rule so routine updates land in one PR, and a rangeStrategy: replace rule so Renovate keeps the siblings floating instead of re-pinning them:
// renovate.json
{
"groupName": "pytorch",
"matchDatasources": ["pypi"],
"matchPackageNames": ["torch", "torchvision", "torchaudio"]
},
{
"matchDatasources": ["pypi"],
"matchPackageNames": ["torchvision", "torchaudio"],
"rangeStrategy": "replace"
}
In pyproject.toml, torch stays pinned; the siblings are floated so uv picks the build compatible with the pinned torch:
# pyproject.toml
ml = [
"torch==2.12.1",
"torchvision",
"torchaudio",
]
uv lock resolved to torchvision 0.27.1 and torchaudio 2.11.0 automatically. A single-package bump now self-heals instead of producing an unsatisfiable set.
The uv lock --check gate stays as the hard backstop. Any broken Renovate PR, including vulnerability alerts that bypass grouping, is blocked from merging regardless of cause.
Promote-on-block for transitive pip CVEs
jupyter-server, jupyterlab, and msgpack are pulled transitively through notebook, jupyter, and librosa, whose own constraints are loose and do not require the fixed versions. The cleanest fix is promoting each to a direct dependency with a lower bound at the fix version:
# pyproject.toml, dev extra
"jupyter-server>=2.20.0", # CVE-2026-44727
"jupyterlab>=4.5.9", # GHSA-vmhf-c436-hxj4
"msgpack>=1.2.1", # GHSA-6v7p-g79w-8964
Once direct, Renovate manages them via osvVulnerabilityAlerts. Future CVEs on these packages get automated PRs.
pnpm overrides for transitive npm CVEs
jsdom@29.1.1 is the latest jsdom and still pins undici ^7.25.0. The lockfile froze undici 7.27.2, which is vulnerable to three high-severity advisories. No parent bump can resolve this until jsdom ships a release pulling in the patched version.
pnpm overrides force the patched version:
# pnpm-workspace.yaml
overrides:
"undici": "7.28.0" # GHSA-vmh5-mc38-953g and related, until jsdom bumps
The pin is exact, not >=7.28.0. The looser form let the resolver jump to undici 8.5.0, which broke jsdom’s ^7.25.0 peer range with a MODULE_NOT_FOUND at test time. 7.28.0 is the latest 7.x and satisfies the peer.
Remove unused dependencies instead of suppressing
pip-audit was passing because of a --ignore-vuln CVE-2025-69872 flag in the CI config. The flagged package was diskcache==5.6.3, and its CVE (pickle-based RCE on cache write) is unpatched upstream. Nothing in src/ or tests/ imported it, and nothing in uv.lock pulled it transitively. Removing the line eliminated both the vulnerable code and the suppression. pip-audit now runs with zero --ignore-vuln flags.
The repository policy now treats --ignore-vuln as a last resort that requires an explicit risk record. The preferred order is to remove the dependency, override it, promote it, or replace it. Some environments may need a time-bounded exception when no fix exists and removal is infeasible, so the runbook records ownership and an expiry condition for that case.
Release-age alignment for security PRs
Renovate’s vulnerabilityAlerts object sets minimumReleaseAge to null by default, which bypasses the repo-wide release-age gate for security fixes2. A fix published minutes earlier would have been installed by a security PR. The override closes that gap:
// renovate.json
"vulnerabilityAlerts": {
"enabled": true,
"labels": ["security"],
"minimumReleaseAge": "1 day"
}
Security fixes now respect the same one-day cooldown as routine updates.
The Dependabot question
I considered adding Dependabot alerts so Renovate would auto-fix transitive npm CVEs. vulnerabilityAlerts consumes GitHub advisory data, and Dependabot’s dependency graph does reach transitive packages. The combination looked like the missing piece.
It is the wrong fit for a Renovate-managed repo. Two reasons.
First, running both duplicates work. Dependabot ships as three features under one name: the dependency graph (detection only), version updates (the updates: block in dependabot.yml), and security updates (a separate repo toggle). Only the graph is purely additive. The PR-making features overlap with Renovate. Setting open-pull-requests-limit: 0 disables version updates but does not disable security updates3. Enabling Dependabot security updates would produce one PR from Dependabot and one from Renovate for the same vulnerability.
Second, the tradeoff is not worth it. The only thing Dependabot alerts add is automatic PR creation for transitive npm vulnerabilities4. The PR still needs review and merge. When no fixed parent exists, the pnpm override is still required. The CI audit gate already catches the vulnerability. Keeping a second tool, a confusingly-named feature set, and careful toggle-gaming to avoid duplicates costs more than it saves.
I kept Renovate as the only dependency tool. The accepted cost is that transitive vulnerabilities require a human to apply the documented one-line fix when CI breaks. This is a known Renovate limitation5. Two of those fixes were a single line each.
Verification
All four CI gates pass clean, locally and in CI:
| Gate | Result |
|---|---|
uv lock --check | passes (was the original torch failure) |
uv run pip-audit | zero findings, zero --ignore-vuln flags |
pnpm audit --audit-level high | zero findings |
pnpm audit signatures | 460 packages verified |
A remediation runbook records the priority order: remove the dependency if unused, add a pnpm override for npm transitive vulns, promote to direct for pip transitive vulns, replace the package if abandoned, and document the risk explicitly as a last resort.
Recurrence: the audit gate’s blast radius
Three weeks later the same pattern returned. pip-audit flagged mistune 3.2.1 (PYSEC-2026-2210..2218, 2652), pulled transitively through nbconvert in the jupyter/notebook dev stack. Renovate stayed silent again: mistune is not a direct dependency. The promote-to-direct runbook applied unchanged: mistune==3.3.0 joined the existing jupyter-server/jupyterlab/msgpack pins in the dev extra, and the gate went clean.
The failure exposed a second structural property the original post did not name. One transitive CVE in the Python backend red-lined every open Renovate PR, including a frontend-only dev-dependencies PR and a mise.toml toolchain PR. checkBackend runs pip-audit on the full resolved tree regardless of which files changed, and the GitHub Actions job runs checkBackend before checkFrontend, so the frontend PR never reached its own (passing) checks. main itself was red on push.
This is the audit gate’s blast radius, distinct from the Renovate grouping blast radius: one vulnerable transitive package blocks N unrelated PRs until a human lands the one-line promote.
The structural fix is now shipped. The single CI job that ran both check-backend and check-frontend on every PR is split into detect + backend + frontend jobs. detect diffs the PR’s base against its head and exports which scopes changed; backend runs only when apps/backend/** (or the shared ci/, .github/workflows/, mise.toml/mise.lock scope) changed, and frontend only when the frontend workspace changed. A frontend-only PR no longer runs pip-audit; a backend-only PR no longer runs pnpm audit. push to main still runs both, so a transitive CVE is still caught on the trunk; it just no longer blocks an unrelated PR. mise.toml/mise.lock are in the shared scope because the toolchain drives both ecosystems, so toolchain PRs run both gates.
What I’d do differently
I wrote the original absolute --ignore-vuln rule before auditing the existing CI config. The diskcache suppression had been there for weeks. The revised policy defines the preferred remediation order and the evidence required for a time-bounded exception. Auditing the code against that policy would have caught the contradiction before review.
The Dependabot investigation was worth doing before committing to single-tool architecture. The duplicate-PR risk is real, and the open-pull-requests-limit: 0 behavior is the kind of detail that bites later. Verifying it against the GitHub docs and the dependabot-core issue tracker confirmed the decision.
References
- Renovate configuration options.
osvVulnerabilityAlerts,vulnerabilityAlerts,minimumReleaseAgedefaults and behavior. - osv-offline. The local OSV database Renovate queries for direct-dependency alerts.
- uv lock. The lockfile gate that caught the partial torch bump.
- pnpm overrides. The mechanism for forcing a transitive version.
- Dependabot options reference.
open-pull-requests-limitsemantics. - dependabot-core#7353. Confirms
open-pull-requests-limit: 0does not affect security updates. - renovatebot/renovate#41825. The open discussion on transitive dependency remediation.
- renovatebot/renovate#33505. Renovate team confirms it is not the right tool for transitive vulnerabilities and points to Mend SCA.
- Dependabot alerts assignable to AI agents. April 2026 changelog on assigning alerts to Copilot, Claude, and Codex for draft fix PRs.
- Dependabot-based dependency graphs for Python. April 2026 changelog on improved transitive dependency trees for Python.
- Dependency auto-submission now supports Python. July 2025 changelog on pip auto-submission.
- Hardening a Renovate Config for Supply Chain Security. The previous post in this series.
Footnotes
-
The
osvVulnerabilityAlertsdocs state this directly: “You will only get OSV-based vulnerability alerts for direct dependencies.” ↩ -
See the
vulnerabilityAlertsdefault values in the Renovate configuration options. ↩ -
From the Dependabot options reference: the open pull request limit “only affects Version Updates which run on a schedule, and not [Security Updates]”. ↩
-
As of April 2026, Dependabot’s dependency graph covers transitive dependencies for npm, Maven, and Python (via auto-submission and Dependabot-based graphs). Detection has improved across ecosystems, but the PR-creation limitation remains: only npm gets automated fix PRs that can bump parent dependencies. For pip and Maven, Dependabot can alert on transitive vulnerabilities but cannot create a PR when the parent package constrains the fix. One exception: since April 2026, Dependabot alerts are assignable to AI agents (Copilot, Claude, Codex), which can analyze a transitive vulnerability and open a draft fix PR. This partially bridges the detection-to-remediation gap for non-npm ecosystems, but it requires manual assignment and does not produce automatic PRs the way Dependabot does for npm. ↩
-
Tracked in renovatebot/renovate#41825. The Renovate team’s own guidance in Discussion #33505 confirms this: “Renovate is not the right tool for transitive vulnerabilities. See https://www.mend.io/sca/ instead.” ↩
This post was written with AI assistance.