feat(dockhand): support transitive dependency overrides/constraints in spec.yaml - #669
Conversation
…n spec.yaml
Renovate version bumps fail the build-containers Grype gate when the bumped
package pins or caps a transitive dependency to a vulnerable version. Add an
optional dependency-override mechanism to the spec.yaml schema, plumbed into the
generated Dockerfile.
- npx: spec.overrides ([]{package, version, reason}) is injected as an npm
"overrides" block in the generated package.json before the npm install step.
- uvx: spec.constraints ([]{spec, reason}) is written to a uv overrides
requirements file and passed to "uv tool install --overrides".
Both injection points match the install step by content (not line number) so
they stay robust to toolhive template formatting. Every entry requires a
non-empty reason (validation fails otherwise) so the justification for
circumventing an upstream pin is auditable in-repo.
Verified end-to-end against the CI build + Grype recipe:
- #469 @brightdata/mcp 2.9.5 + override @modelcontextprotocol/sdk 1.26.0:
resolves to SDK 1.26.0, grype --fail-on high --only-fixed passes.
- #527 mcp-clickhouse 0.3.0 + constraint fastmcp>=3.2.0: fastmcp 3.4.0,
import mcp_clickhouse OK, grype passes.
Refs #668
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🔒 MCP Security Scan Results✅ adb-mysql-mcp-server
Summary: Scanned 1 MCP server(s), all passed security checks. ✅ |
🛡️ Skill Security Scan Results |
next-devtools-mcp hard-pins SDK 1.25.2 which fails the Grype gate for GHSA-345p-7cg4-v4c7. Bump via npm overrides (same major, fixed in 1.26.0). Also cherry-picks dockhand override support from #669. Co-authored-by: Cursor <cursoragent@cursor.com>
# Conflicts: # cmd/dockhand/main.go
…drift The injection anchors are matched against toolhive's generated Dockerfile, but the tests only exercised hand-written samples that mirror it, so drift in the real template would pass tests and silently ship an image with no overrides applied. - Match the uvx install step on the bare "uv tool install" verb instead of requiring the quoted package spec to follow immediately. toolhive's template conditionally emits its own flags in between (RuntimeConfig .BuildWith renders as "--with '<spec>'"), which the old anchor missed entirely. - Parse the package.json payload toolhive emits and add the overrides key to it, rather than rebuilding the file from hardcoded name/version constants. Any other field toolhive puts there is now preserved instead of silently dropped. - Add tests that inject into Dockerfiles generated by the pinned toolhive version, covering the uvx step both with and without build-time constraints. These fail on anchor drift; they catch the --with case the previous anchor could not.
The security scan runs the package directly rather than the built image, so it never saw the overrides injected into the Dockerfile and exercised a different dependency set than the one that ships. Pass spec.constraints through to the scanner as a uv overrides requirements file (uv takes a file, not inline specifiers), written to a temp file for the duration of the scan. npx spec.overrides are not reapplied: npm honors "overrides" only from a package.json it installs into, and the scan has no project directory. That is safe for the intended use case, since swapping a vulnerable but working dependency changes neither startup nor the tool surface being analyzed, but log a note so a future startup-affecting override does not fail confusingly.
adb-mysql-mcp-server depends on mcp[cli]>=1.8.0 with no upper bound. mcp 2.0.0 removed the mcp.server.fastmcp module this server imports at startup, breaking both the container build and the smoke test canary in build-containers.yml. This is also the first spec.yaml to exercise the override mechanism, so CI now actually covers it end to end rather than only unit tests.
Override values were interpolated into RUN lines inside unescaped single
quotes. A PEP 508 requirement legitimately contains single quotes in an
environment marker, so
fastmcp>=3.2.0; python_version < '3.14'
was written to the overrides file as
fastmcp>=3.2.0; python_version < 3.14
which is no longer a valid marker. Anything following the quote also ran
as shell at image build time, so a spec value was able to execute
arbitrary commands during the build. The npm path had the same flaw via
the echoed package.json payload.
- Add shellSingleQuote and use it for both the uv override specs and the
npm package.json payload, escaping embedded quotes as '\''.
- Reject control characters in override/constraint values. Quoting makes
shell metacharacters inert, but a newline would still terminate the RUN
instruction, and none of these fields has a legitimate use for one.
- Cover quoted markers, embedded quotes, and injection attempts. These
execute the emitted line through a real shell and compare the file it
writes, rather than assuming how the line parses.
|
Picking this up since it had been sitting a while, and the Merged Added on top of the original commit:
Verification: This supersedes #827, which took the narrower approach of wiring through toolhive's native |
The new test cases pushed "1.26.0" and "injection attempt" past goconst's occurrence threshold, failing CI lint.
@neondatabase/mcp-server-neon is deprecated on npm in favor of the hosted server at mcp.neon.tech. Its last publish was 0.6.5 on 2026-03-02, which is already the version pinned here, so there is nothing to bump to. Upstream's main is now a Next.js app for the hosted service and is never published under this npm name. The practical effect is that this server can never pass the Grype gate: its abandoned tree exact-pins roughly 30 packages and produces 86 findings, including a critical in next (GHSA-9qr9-h5gf-34mp) and about 34 highs. Clearing that would take ~30 override entries against a package that will never be updated, which is not a defensible use of the mechanism added in #669. The remote server is already in toolhive-catalog. Removing the spec does not delete the published image, it just stops carrying it forward. Closes #835 Co-authored-by: Dan Barr <6922515+danbarr@users.noreply.github.com>
* fix(adb-mysql-mcp-server): force cryptography>=50.0.0 Grype flags cryptography 48.0.1 for GHSA-g6cj-pr64-35w5 (High): the PKCS#7 EnvelopedData decryption path exposes a Bleichenbacher oracle through distinguishable errors and timing. The affected range is >=44.0.0,<50.0.0 and the fix is 50.0.0. alibabacloud-tea-openapi caps cryptography <49.0.0 for python>=3.9, which excludes the fixed version, so the constraint is needed to pull it forward. This is not a regression from the mcp<2 constraint; cryptography resolves to 48.0.1 with and without it. .grype.yaml treats runtime dependencies under /opt/uv-tools as not ignorable, so an ignore rule is not the right instrument here. Asking upstream to raise the cap is the more correct long-term fix but is not something we control. Verified locally: image installs cryptography 50.0.0 and mcp 1.29.0, grype --fail-on high --only-fixed reports no vulnerabilities, the container starts and activates 3 tools plus 4 resources, and the MCP security scan passes with all tools SAFE. Residual risk: imports and the RSA signing primitive are covered, but not live API calls against AnalyticDB, which need credentials. Fixes #828 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(chroma-mcp): constrain mcp[cli] to >=1.28.1 for 5 HIGH CVEs chroma-mcp 0.2.6 exact-pins mcp[cli]==1.6.0, which excludes the fix for all five HIGH advisories Grype reports against the published image: GHSA-3qhf-m339-9g5v (fixed 1.9.4), GHSA-j975-95f5-7wqh (1.10.0), GHSA-9h52-p55h-vw2f (1.23.0), GHSA-jpw9-pfvf-9f58 (1.27.2) and GHSA-vj7q-gjh5-988w (1.28.1). 1.28.1 is the lowest floor that clears all five. A smaller bump to 1.23.0 would newly expose GHSA-hvrp-rf83-w775 (HIGH, affects >=1.23.0,<=1.27.1), which the pinned 1.6.0 predates. The constraint is capped <2 so uv cannot resolve the mcp 2.x major. chroma-mcp imports only mcp.server.fastmcp.FastMCP, which is stable across the 1.x line, so this stays well inside same-major territory. Verified: image builds, resolves mcp 1.29.0, grype --fail-on high --only-fixed reports no vulnerabilities, and the mcp-scan enumerates all 13 chroma_* tools with no findings. Refs #830 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(brightdata-mcp): override SDK to 1.26.0 to clear 3 high CVEs @brightdata/mcp 2.9.0 exact-pins @modelcontextprotocol/sdk 1.21.2, which fails the Grype gate with three high findings: GHSA-w48q-cv73-mx4w DNS rebinding fixed 1.24.0 GHSA-8r9q-7v3j-jr4g ReDoS fixed 1.25.2 GHSA-345p-7cg4-v4c7 >=1.10.0,<=1.25.3, fixed 1.26.0 An exact pin below every fix, so this is the override shape #669 introduced. 1.26.0 is the lowest version clearing all three: 1.25.2 is still inside the GHSA-345p range. Same-major, no API break. Verified: grype --fail-on high --only-fixed exits 0 with no high/critical findings, npm resolves 1.26.0 for both the direct dependency and fastmcp's copy (deduped), and the container starts and enumerates its tool list over stdio. Refs #830 * fix(mcp-jetbrains): override SDK to 1.26.0 to clear 2 high CVEs No code-scanning alerts landed for this server, so the findings were established by building the image and running grype locally: @modelcontextprotocol/sdk 1.7.0 GHSA-w48q-cv73-mx4w DNS rebinding fixed 1.24.0 high GHSA-8r9q-7v3j-jr4g ReDoS fixed 1.25.2 high @jetbrains/mcp-proxy 1.8.0 exact-pins the SDK at 1.7.0, so this is the same shape as onchain-mcp. 1.26.0 rather than 1.25.2 because GHSA-345p-7cg4-v4c7 affects >=1.10.0,<=1.25.3, which 1.7.0 predates; a smaller bump would trade two advisories for a third. Same-major. Verified: grype --fail-on high --only-fixed exits 0, npm reports @modelcontextprotocol/sdk@1.26.0 overridden, and the proxy completes the MCP initialize handshake. tools/list then returns "No working IDE endpoint available", which is the expected response with no JetBrains IDE to proxy to and the reason this server carries insecure_ignore. Refs #830 * fix(astra-db-mcp): override undici to 6.28.0 to clear 3 high CVEs undici 5.29.0 reaches the image through astra-db-mcp -> mcp-evals -> @actions/core -> @actions/http-client, which caps it at ^5.25.4: GHSA-vrm6-8vpv-qv8q fixed 6.24.0 high GHSA-v9p9-hfj2-hcw8 fixed 6.24.0 high GHSA-vxpw-j846-p89q fixed 6.27.0 high The 5.x line is end of life, so no 5.x release can carry these fixes and the cap excludes every fix. 6.28.0 is the last 6.x release; no high advisory affects >=6.27.0 on that line, and stopping at 6.x avoids the 7.x-only advisories (GHSA-hm92-r4w5-c3mj, GHSA-vmh5-mc38-953g, GHSA-4cwx-7wf7-3272). This is a cross-major force, so worth flagging. It is low risk here: @actions/http-client 4.0.1 upstream has itself moved to undici ^6.23.0, and the consumer is the mcp-evals harness rather than the server's request path. Verified: grype --fail-on high --only-fixed exits 0 with only medium and low findings remaining, npm reports undici@6.28.0 overridden, and scripts/mcp-scan/run_scan.py exits 0 with all 19 tools enumerated and SAFE. Refs #830 * fix(browserbase-mcp-server): override sharp and undici to clear 4 high CVEs Grype reports two capped dependencies, not just the sharp one #830 recorded: sharp 0.33.5 (capped ^0.33.0) GHSA-f88m-g3jw-g9cj libvips CVEs, affects <0.35.0 high undici 5.29.0 (capped ^5.29.0 by @ai-sdk/provider-utils, reached via @browserbasehq/stagehand -> @ai-sdk/amazon-bedrock) GHSA-vrm6-8vpv-qv8q fixed 6.24.0 high GHSA-v9p9-hfj2-hcw8 fixed 6.24.0 high GHSA-vxpw-j846-p89q fixed 6.27.0 high Both caps exclude every fix. sharp goes to 0.35.3, the newest 0.35.x and the only sharp advisory in play. undici goes to 6.28.0, the last 6.x release, which clears all three without stepping into the 7.x-only advisories. Both are effectively cross-major forces: sharp 0.x minors are breaking by convention, and undici 5 to 6 is a real major. Mitigating evidence is that @ai-sdk/provider-utils 5.x upstream now uses undici ^7.28.0, so that consumer tracks well past 6.x. Verified: grype --fail-on high --only-fixed reports "No vulnerabilities found" and exits 0; sharp loads in the container against libvips 8.18.3 and round-trips a create/resize/re-encode; the server completes the MCP initialize handshake and enumerates its tool list over stdio. Note, unrelated to this change: @browserbasehq/mcp-server-browserbase 2.4.3 is currently uninstallable from a clean npm cache. Its transitive ai@5.0.228 exact-pins @ai-sdk/gateway@2.0.127, which does not exist in the registry. This reproduces with a bare `npm install` and no overrides, and is the likely cause of the intermittent build-step failures. CI will keep failing at npm install until upstream publishes a fixed release. Refs #830 --------- Co-authored-by: Dan Barr <6922515+danbarr@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add packaging for next-devtools-mcp v0.4.0 (Vercel), rebased onto main now that dockhand transitive dependency overrides (#669) have landed. Package: https://www.npmjs.com/package/next-devtools-mcp Repository: https://github.com/vercel/next-devtools-mcp Pins @modelcontextprotocol/sdk to 1.26.0 via spec.overrides to clear GHSA-345p-7cg4-v4c7 from the upstream 1.25.2 hard-pin. Co-authored-by: Cursor <cursoragent@cursor.com>
Add packaging for next-devtools-mcp v0.4.0 (Vercel), rebased onto main now that dockhand transitive dependency overrides (#669) have landed. Package: https://www.npmjs.com/package/next-devtools-mcp Repository: https://github.com/vercel/next-devtools-mcp Pins @modelcontextprotocol/sdk to 1.26.0 via spec.overrides to clear GHSA-345p-7cg4-v4c7 from the upstream 1.25.2 hard-pin. Co-authored-by: Cursor <cursoragent@cursor.com>
Implements the dependency-override mechanism from #668.
What
Renovate version bumps fail the
build-containersGrype gate (--fail-on high --only-fixed) when the bumped package pins/caps a transitive dependency to a vulnerable version. This adds an optional override mechanism tospec.yaml, plumbed into the generated Dockerfile. toolhive needs no changes — itsBuildFromProtocolSchemeWithName(..., dryRun=true)returns the Dockerfile as a string, which is the injection seam.Schema (
cmd/dockhand/main.go)Overrides []OverrideEntry{Package, Version, Reason}(npx only)Constraints []ConstraintEntry{Spec, Reason}(uvx only)Reasonis mandatory — validation fails otherwise, mirroringsecurity.allowed_issues.Injection
RUN echo '{...}' > package.jsonstep to embed anoverridesblock (npm honorsoverridesonly from package.json), kept before thenpm installstep.RUN printf ... > /tmp/uv-overrides.txtstep before the install step and adds--overrides /tmp/uv-overrides.txtto theuv tool installinvocation. The install line is matched by content, ignoring comment lines that merely mentionuv tool install.Verification (end-to-end: dockhand-generated Dockerfile -> docker build -> grype --fail-on high --only-fixed)
@modelcontextprotocol/sdk1.26.0fastmcp>=3.2.0import mcp_clickhouseOK; grype gate passesfastmcp>=3.2.0import mcp_neo4j_cypherOK; grype passes — but crosses upstream's deliberate<2.14cap (major 2->3); functional testing of the neo4j tools is required before relying on it.go build ./...,go vet,go test ./cmd/dockhand/..., andgolangci-lint run ./cmd/dockhand/...all pass.Follow-up
This PR only adds the mechanism — it does not modify the three renovate spec.yaml files. After this merges to
main, each renovate PR (#469, #527, #528) needs to be rebased on main and have its override/constraint block added to itsspec.yaml.Refs #668
🤖 Generated with Claude Code