Development record
Sub-finding of GHSA-4q78-ccfv-f374 that does not need the design decision: `ahoy install` uses any CLAUDE_PLUGIN_DATA value as the verified cache, including one that is relative, lies inside the repository being installed, or is world-writable. `internal/core/ahoy/data_dir.go:pluginDataDir` returns the env value unexamined and `owned_copy.go:ownedCopySourceReady` plus `apply.go:installOwnedEntry` copy from it. The harness's persistent per-plugin data directory (spc-35) is always absolute, never inside a project checkout, and never world-writable, so refusing those shapes is a strict hardening of the current contract: a relative value resolves against the checkout the verb runs in, an in-checkout value blesses committed bytes as the owned PATH binary, and a world-writable cache lets any local user swap both artefact and record. The fix must establish that such a data dir is ignored with a note naming why, that install then degrades to the existing loud pinned-symlink path exactly as when no cache exists, and that a harness-shaped data dir still installs the owned copy; the display-only readers of the same variable (`vintage.go:readPinnedTag`, the skew notice) make no trust decision and are unchanged.
Sub-finding of GHSA-4q78-ccfv-f374 that does not need the design decision: ahoy install uses any CLAUDE_PLUGIN_DATA value as the verified cache, including one that is relative, lies inside the repository being installed, or is world-writable. internal/core/ahoy/data_dir.go:pluginDataDir returns the env value unexamined and owned_copy.go:ownedCopySourceReady plus apply.go:installOwnedEntry copy from it. The harness's persistent per-plugin data directory (spc-35) is always absolute, never inside a project checkout, and never world-writable, so refusing those shapes is a strict hardening of the current contract: a relative value resolves against the checkout the verb runs in, an in-checkout value blesses committed bytes as the owned PATH binary, and a world-writable cache lets any local user swap both artefact and record. The fix must establish that such a data dir is ignored with a note naming why, that install then degrades to the existing loud pinned-symlink path exactly as when no cache exists, and that a harness-shaped data dir still installs the owned copy; the display-only readers of the same variable (vintage.go:readPinnedTag, the skew notice) make no trust decision and are unchanged.
Grounds
- pursued: the harness never produces these shapes, so refusing them is a strict hardening that costs a real install nothing and needs no decision; binding the cache to an attestation the environment cannot supply stays open on the parent record GHSA-4q78-ccfv-f374 (iss-2609012039102770)
Frontmatter
| id | iss-2609012039116200 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | minor |
.abcd/work/issues/resolved/iss-2609012039116200-install-accepts-relative-in-checkout-or.md
open on GitHub ↗ · commit history ↗
mentions2
- iss-2609012039102770GHSA-4q78-ccfv-f374 (CWE-345, advisory severity low): the owned PATH-copy verification is self-referential under an env-supplied CLAUDE_PLUGIN_DATA. `internal/core/ahoy/data_dir.go:pluginDataDir` is `os.Getenv("CLAUDE_PLUGIN_DATA")` and nothing else; `owned_copy.go:cacheRecordedSHA` reads `binary_sha256` from a `binary-meta` beside the artefact under that same directory; `apply.go:installOwnedEntry` hashes the artefact, compares it to that record, writes the bytes 0755 as the owned PATH entry and records provenance in `~/.abcd/path-entry`. The bootstrap's manifest authentication (`hooks/bootstrap.sh`, cache_trust=manifest, iss-2608210934566228) covers only the cache-to-plugin-root promotion; the cache-to-PATH promotion re-verifies against the co-located record only, which an attacker who writes both satisfies trivially — adr-46 decision 3 states exactly this principle for the bootstrap. Reproduced at v0.7.0: with CLAUDE_PLUGIN_DATA and ABCD_PLUGIN_ROOT pointed at attacker-chosen directories, `ahoy install --yes --adopt` installed a one-byte file as `~/.local/bin/abcd`, recorded its hash and the fake plugin root in path-entry, and `classifyBinTarget` then vouched for it as an owned copy.
- spc-35hook-binary-to-persistent-data-dir