Skip to content

fix(security): validate package spec before scanner source fetch (MCP-2442 P0)#672

Merged
Dumbris merged 2 commits into
mainfrom
sec/mcp-2442-validate-package-spec
Jun 15, 2026
Merged

fix(security): validate package spec before scanner source fetch (MCP-2442 P0)#672
Dumbris merged 2 commits into
mainfrom
sec/mcp-2442-validate-package-spec

Conversation

@Dumbris

@Dumbris Dumbris commented Jun 15, 2026

Copy link
Copy Markdown
Member

[SEC/P0] Scanner fetch executes untrusted setup.py for non-registry specs

Source: Codex security audit of the scanner program (PRs #653#670). internal/security/scanner/package_fetch.go.

Bug

resolveFromPackageFetch passed the server's configured package spec to pip download / uv pip download / npm pack UNVALIDATED. For a uvx/pipx server configured with a local-path, file:, URL, or VCS (git+…, [email protected]:…) spec, pip download <spec> --only-binary=:all: STILL executes setup.py (PEP 517 build backend, egg_info metadata resolution) during what is meant to be a static scan — arbitrary code execution from untrusted server config.

--only-binary=:all: alone protects only bare registry-name specs; that was the gap.

Fix

New validatePackageSpec(ecosystem, spec): require a bare PEP 503 (Python) / npm registry name (optionally version-pinned). Reject paths, file:, URLs, and VCS/ssh specs (:/\ markers, path prefixes, .. traversal). resolveFromPackageFetch gates both the npm and python fetch paths through it and falls back to tool_definitions_only (no fetch, no execution) on rejection.

  • The already-safe registry-name path is unchanged (npm pack --ignore-scripts + wheel-only --only-binary=:all:).

Tests (TDD — written failing first)

  • TestValidatePackageSpec — registry names pass; local-path / git+ / ssh / URL / file: / traversal / drive-letter specs rejected (npm + python).
  • TestResolveFromPackageFetch_RejectsNonRegistrySpec — a local-path / git+ uvx spec and a ./local npx spec return an error (fallback) and a setup.py execution marker stays absent.

Verification

  • go build ./...
  • go test ./internal/security/... -race
  • golangci-lint (CI config, v2) — 0 issues ✅

Docs: docs/features/security-scanner-plugins.md updated to document the registry-name requirement.

Related MCP-2442

🚫 Do not merge — pre-merge gate is human-owned (Gate 3).

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 15, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: f6135c9
Status: ✅  Deploy successful!
Preview URL: https://756c9a06.mcpproxy-docs.pages.dev
Branch Preview URL: https://sec-mcp-2442-validate-packag.mcpproxy-docs.pages.dev

View logs

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: sec/mcp-2442-validate-package-spec

Available Artifacts

  • archive-darwin-amd64 (28 MB)
  • archive-darwin-arm64 (25 MB)
  • archive-linux-amd64 (16 MB)
  • archive-linux-arm64 (14 MB)
  • archive-windows-amd64 (28 MB)
  • archive-windows-arm64 (25 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (21 MB)
  • installer-dmg-darwin-arm64 (19 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 27536590163 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@codecov-commenter

codecov-commenter commented Jun 15, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 78.57143% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/security/scanner/package_fetch.go 78.57% 4 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@Dumbris

Dumbris commented Jun 15, 2026

Copy link
Copy Markdown
Member Author

codex review (gpt-5.5) → REQUEST_CHANGES (independently re-verified against the diff)

Blocking bug: validatePackageSpec validates only the parsed name, not the version/direct-reference tail. A PEP 508 direct reference like pkg@./local or pkg@/abs/path passes the guard (no :/\, doesn't start with .///~; parsePackageSpec splits on the last @ → name pkg is a valid PEP 503 name). The original unsafe spec then flows into uv pip download/pip download, which builds the local-path direct reference and executes setup.py — exactly what MCP-2442 is meant to prevent.

Fix: validate the whole spec including the @-tail — reject any direct-reference/local-path/VCS form, not just the parsed name. Add tests for name@./local, name@/abs, name@git+... bypasses.

Dumbris and others added 2 commits June 15, 2026 12:20
The scanner's published-source fetch passed the server's configured
package spec to `pip download` / `uv pip download` UNVALIDATED. For a
local-path, `file:`, URL, or VCS (`git+…`, ssh) spec, those commands
still invoke the package's setup.py / PEP 517 build backend to resolve
metadata — EVEN with `--only-binary=:all:` — executing untrusted code
from the server config on the meant-to-be-static scan path.

Add validatePackageSpec(ecosystem, spec): require a bare PEP 503 /
npm registry name (optionally version-pinned); reject paths, `file:`,
URLs, and VCS/ssh specs. resolveFromPackageFetch now gates both the
npm and python fetch paths through it, falling back to
tool_definitions_only (no fetch, no execution) on rejection. The
already-safe registry-name path is unchanged.

TDD: TestValidatePackageSpec (registry pass / non-registry reject) and
TestResolveFromPackageFetch_RejectsNonRegistrySpec (setup.py execution
marker stays absent for a local-path/git+ spec).

Related MCP-2442
…ypass

Codex re-review of #672 found validatePackageSpec only validated the
parsed NAME, not the version/@-tail. A PEP 508 / npm direct reference
("pkg@./local", "pkg@/abs/path", "pkg@git+https://…") therefore passed
— the name "pkg" is a valid registry name — while the full untrusted
spec still reached `pip download` / `uv pip download` / `npm pack` and
executed setup.py. The P0 was not actually closed.

Validate the WHOLE spec:
- New bareVersionRe: the "name@<tail>" / "name==<tail>" tail must be a
  bare version / range / npm dist-tag — must start alphanumeric and
  contain no '/' or ':' — so a path/URL/VCS tail is rejected.
- Python: any '@' is a PEP 508 direct reference (pins use '=='), so it
  is refused outright.
- Bare version pins / dist-tags (pkg@1.2.3, pkg@latest, @scope/pkg@2.0.0)
  stay valid — no regression.

TDD: TestValidatePackageSpec gains npm+python "pkg@<path/url/vcs>" reject
cases and bare-version positive cases; TestResolveFromPackageFetch_
RejectsNonRegistrySpec gains uvx/npx direct-reference cases asserting the
setup.py execution marker stays absent.

Related MCP-2442

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@Dumbris Dumbris force-pushed the sec/mcp-2442-validate-package-spec branch from d3d873a to f6135c9 Compare June 15, 2026 09:22
@Dumbris

Dumbris commented Jun 15, 2026

Copy link
Copy Markdown
Member Author

Re-review fix pushed — direct-reference @-tail bypass closed

Thanks for the catch. Confirmed: validatePackageSpec previously validated only the parsed name, so a PEP 508 / npm direct reference slipped through.

Fix (commit f6135c90, force-pushed to sec/mcp-2442-validate-package-spec, rebased on latest origin/main): validate the whole spec including the @-tail.

  • New bareVersionRe: the name@<tail> / name==<tail> tail must be a bare version / range / npm dist-tag — must start alphanumeric and contain no / or : → path/URL/VCS tails rejected.
  • Python: any @ is a PEP 508 direct reference (pins use ==) → refused outright.
  • Bare pins/dist-tags (pkg@1.2.3, pkg@latest, @scope/pkg@2.0.0) stay valid — no regression.

TDD (failing first, now green):

  • TestValidatePackageSpec — added npm+python reject cases: pkg@./local, pkg@/abs/path, pkg@~/evil, pkg@git+https://…, pkg@file:…, @scope/pkg@./local, flights @ git+https://…; plus positive pkg@1.2.3 / pkg@latest / @scope/pkg@2.0.0.
  • TestResolveFromPackageFetch_RejectsNonRegistrySpec — added uvx evilpkg@<localdir> + npx evilpkg@./local-evil; asserts setup.py execution marker stays absent.

Verified: go build ./... ✅ · go test ./internal/security/... -race ✅ · golangci-lint v2 (CI config) 0 issues ✅.

Re-run CI + Codex against f6135c90.

@mcpproxy-gatekeeper mcpproxy-gatekeeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

codex re-review ACCEPT (gpt-5.5): validatePackageSpec now checks the FULL original spec incl. the @-tail before any download; all direct-reference bypass forms (pkg@./local, pkg@/abs, pkg@git+..., pkg@file:, URLs, VCS, python @ direct refs) rejected; legitimate pins/dist-tags (pkg@1.2.3, pkg@latest, @scope/pkg@2.0.0, pkg==1.0) still pass; setup.py-exec path unreachable for bypass forms. Closes the P0 hole from round 1.

@Dumbris Dumbris merged commit e034437 into main Jun 15, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants