* test(#3514): add failing-first denylist and integrity suites * fix(#3514): deny internal fetch hosts; disclose unverified integrity * docs(#3514): trust-model, glossary, and changeset entries * fix(#3514): scope v6 checks to literals; exact pin kinds in prompt * chore(#3514): backfill changeset pr number --------- Co-authored-by: sim <sim@local>
4.4 KiB
Fix PR
Using the wrong template? — Enhancement: use enhancement.md — Feature: use feature.md
Linked Issue
Required. This PR will be auto-closed if no valid issue link is found.
Fixes #3514
The linked issue must have the
confirmed-buglabel. If it doesn't, ask a maintainer to confirm the bug before continuing.
What was broken
Three hardening gaps at the ADR-1244 D3/D5 edges (epic #1900, finding F21): the capability URL-import fetcher never validated the resolved host (a loopback or cloud-metadata https endpoint was fetched like any URL); an http:// tarball spec failed with a raw ERR_INVALID_PROTOCOL instead of a named reason; and an install without an integrity pin produced a consent prompt that did not distinguish verified from unverified content.
What this fix does
- Pre-transport fetch gate (
realHttpsGet→assertFetchableUrl): loopback/link-local/metadata/unspecified hosts (127/8,169.254/16incl.169.254.169.254,0/8,::1,fe80::/10,::, IPv4-mapped spellings in both dotted and hex-normalized forms) andlocalhost/*.localhostnames are denied with a named error before any bytes leave — the injected transport is provably never invoked (tests assert call-count 0). The WHATWG URL parser normalizes alternate IP spellings (decimal2130706433, hex0x7f000001, octal, short forms) to dotted-quad before the gate sees them — locked by tests. - Non-https, per the adopted split decision:
parseSpecstill classifieshttp://tarball specs (internal-mirror flows are not broken at parse); the fetcher refuses with a clear https-only reason naming the mirror alternative. - Unverified-integrity disclosure:
evaluateInstallTrustacceptsintegrityPinned(a verified--integritypin, or a git#sha:<commit>ref); the consent prompt renderscontent: NO PINNED HASH — staged unverifiedwhen no pin was supplied (and the pinned counterpart when one was). The status is prompt-only — deliberately excluded fromdisclosureSignature(consent's content binding isbundleContentHash, #1459; tests lock that it can never fire a spurious re-consent). Legacy callers see byte-identical output.
Root cause
The D3 fetcher was built with a byte cap and a timeout but no host policy — the finding is an edge the pipeline's safe posture simply wasn't applied to. The integrity line was missing because the disclosure was built from the manifest alone; the pin fact lives on the resolve path, and nothing threaded it into the verdict.
Testing
How I verified the fix
- Failing-first:
gsd-testat the tests-only commit (holodeck, linux-node24) — verdictfailedwith exactly the 20 expected failures (12-case denylist matrix, https-reason, trust/lifecycle integrity suites), 34,343 green. - GREEN: full
gsd-testmatrix at the final HEAD — verdict below. - Controls lock the negative space: a public host and a private-range mirror still fetch through the gate;
parseSpecstill classifieshttp://; legacysummarizeDisclosureoutput is line-identical.
Regression test added?
- Yes — added a test that would have caught this bug
Platforms tested
- macOS
- Windows (including backslash path handling)
- Linux
Runtimes tested
- Claude Code
- Gemini CLI
- OpenCode
- Other: ___
- N/A (not runtime-specific — engine-internal source resolver + trust gate)
Checklist
- Issue linked above with
Fixes #3514— PR will be auto-closed if missing - Linked issue has the
confirmed-buglabel - Fix is scoped to the reported bug — no unrelated changes included
- Regression test added (or explained why not)
- All existing tests pass (
npm test) — fullgsd-testmatrix at final HEAD .changeset/fragment added —Securitytype- No unnecessary dependencies added
Breaking changes
None intended. http:// tarball URLs already failed at the transport; they now fail with a better message. Denied hosts (loopback/link-local/metadata/localhost) are not legitimate install sources. Known limits, documented in docs/explanation/capability-trust-model.md: RFC1918 private ranges are deliberately not denied (internal mirrors); the check is on the URL's host literal — DNS rebinding is out of scope. Local/unpinned-git installs render the unverified line even though their trust basis is the path/commit (honest, slightly noisy).