Use HTTPS for Docker catalog assets#518
Conversation
saucow
left a comment
There was a problem hiding this comment.
Review — HTTPS for Docker catalog assets
LGTM, and it's a strictly-safer change. UpgradeKnownHTTPURLToHTTPS is correctly scoped: http→https only for the exact host desktop.docker.com on port `` or 80, every other URL passes through unchanged, and it doesn't touch `allowInsecure` — so the SSRF denylist, IP pinning, and redirect re-validation in `NewDirectHTTPClient` stay fully active on the upgraded request. A malicious catalog supplying `http://attacker/` stays `http` and is still rejected with "remote URL must use https". All four call sites upgrade before the guarded fetch.
Recommendation: approve-with-nits — test-completeness only, inline.
One follow-up that's outside this PR's diff: cmd/docker-mcp/server/inspect.go (the other server inspect path) still fetches ToolsURL/ReadmeURL via a raw &http.Client{Transport: desktop.ProxyTransport()} (:111, :145), not through remoteurl. No regression — that path never enforced https — but the "HTTPS for catalog assets" goal is only partially realized until that sibling fetch is migrated too. Worth a tracking issue.
| // UpgradeKnownHTTPURLToHTTPS rewrites legacy public HTTP URLs that are known to | ||
| // serve the same content over HTTPS. It deliberately does not relax validation | ||
| // for arbitrary HTTP URLs. | ||
| func UpgradeKnownHTTPURLToHTTPS(rawURL string) string { |
There was a problem hiding this comment.
Nicely scoped — exact-host match (EqualFold) + port `` / 80 only, and not touching `allowInsecure` keeps the SSRF/IP-pinning guard live on the upgraded request. Host-confusion vectors all fail closed: `evil.desktop.docker.com` and `desktop.docker.com.evil.example` don't match the host so stay `http` (then rejected downstream), and `http://desktop.docker.com@attacker.example/` keeps `attacker.example` as the host so also stays `http`. No change requested here — just confirming the boundary is right.
| err := inspectServer(ctx, dao, catalogObj.Ref, "my-server", workingset.OutputFormatJSON, func(_ context.Context, url string) ([]byte, error) { | ||
| assert.Equal(t, readmeURL, url) | ||
| return []byte(readmeContent), nil | ||
| }) |
There was a problem hiding this comment.
TestInspectServerWithReadme now injects a fake fetchReadme that only asserts URL equality, so it no longer exercises the real fetch.Untrusted path (and therefore the new upgrade + the https/SSRF guard) on the inspect flow this PR fixes. And inspectServer fetches the persisted ReadmeURL as-is (server.go:62-65) — normalizeCatalogServerURLs only runs at create time — so for any catalog persisted before this PR, fetch.Untrusted's internal upgrade is the only thing rescuing an http://desktop.docker.com readme at inspect time.
The upgrade itself is well covered at the unit seam (remoteurl_test.go, fetch_test.go::TestUntrustedRejectsUnknownHTTPURL, create_test.go), so this is minor — but one end-to-end assertion through the exported InspectServer (or fetch.Untrusted directly) with an http://desktop.docker.com readme would restore a regression tripwire for the exact inspect scenario the PR is fixing.
saucow
left a comment
There was a problem hiding this comment.
Approving — this is strictly safer than the prior state: the http→https rewrite is host-pinned to desktop.docker.com and doesn't touch allowInsecure, so the SSRF/IP-pinning hardening stays fully active. The items in my earlier review are test-completeness nits plus a follow-up for the sibling server inspect fetch path — none are blocking. Nice fix.
Summary
http://desktop.docker.comURLs to HTTPS before guarded direct fetchesRoot cause
catalog-next server inspectfetchessnapshot.server.readmebefore returning JSON. The v0.43.0 switch toremoteurl.NewDirectHTTPClientcorrectly rejects plain HTTP, but Docker catalog snapshots still contain README URLs such ashttp://desktop.docker.com/mcp/catalog/v3/readme/..., so Pinata overview/tools tabs fail on inspect.Validation
go test ./pkg/remoteurl ./pkg/fetch ./pkg/catalog ./pkg/catalog_nextgo test ./cmd/docker-mcp/commands -run 'Test.*Mcpregistry|Test.*Catalog|Test.*Gateway'