Skip to content

Harden app-store install path and cap-state reads#315

Merged
TeoSlayer merged 2 commits into
mainfrom
security/appstore-install-hardening
Jun 22, 2026
Merged

Harden app-store install path and cap-state reads#315
TeoSlayer merged 2 commits into
mainfrom
security/appstore-install-hardening

Conversation

@TeoSlayer

Copy link
Copy Markdown
Collaborator

Three app-store security fixes in cmd/pilotctl.

1. Path traversal during staging

appstore.go: manifest binary.path was joined onto the bundle and staging dirs with bare filepath.Join — a path like ../../etc/x escaped the staging tree, letting a hostile manifest read an arbitrary host file as the "binary" or plant a copy outside staging. Added a resolveUnder containment guard (mirrors the supervisor's) at both join sites; any path resolving outside the target dir is refused.

2. Unbounded catalogue bundle download

appstore_catalogue.go: the bundle tarball was copied with no size cap, so a hostile/compromised CDN could stream an unbounded body and exhaust disk before the post-copy sha256 check ran. Added an io.LimitReader cap (maxBundleBytes, 128 MiB compressed) that fails closed when exceeded. The per-file untar cap already existed; this bounds the download itself.

3. Cap-state reads fail open

loadCapStateRecords silently skipped malformed lines and tampered HMAC records — under-reporting usage and making an erased cap look healthy. It now returns an error; appstore caps fails closed (integrity_error) on a malformed line, an HMAC mismatch, or a signed chain spliced with an unauthenticated record. Legacy (no-HMAC) and nil-key reads are still accepted for read-only reporting.

Tests

  • TestResolveUnderRejectsTraversal../../etc/x, ../escape, a/../../escape, absolute, and empty paths rejected; nested relatives accepted.
  • TestFetchAndUnpackBundle_RejectsOversizedDownload — oversized download refused, within-cap unpacks (maxBundleBytes is a var so the test avoids writing 128 MiB).
  • TestLoadCapStateRecordsMalformedLineFailsClosed + updated TestLoadCapStateRecordsHMACChain — fail-closed on garbage/tamper/mixed, legacy + nil-key still accepted.

Validation

GOWORK=off go vet ./... clean; go build ./cmd/daemon ./cmd/pilotctl; go test -short ./cmd/pilotctl/... and go test -race -short ./cmd/pilotctl/ green; gofmt clean on touched files.

Cross-repo follow-up

Item 3 (HMAC) interoperates with pilot-protocol/wallet#22 (writer). The reader here already understood the hmac field; both derive the key via the same HKDF (info=pilot-cap-state-v1). No version bump required — this PR has no new dependency on an unpublished wallet/app-store release.

Three install-path security fixes in pilotctl appstore:

- Path traversal during staging: manifest binary.path was joined onto
  the bundle and staging dirs with no containment check, so a path like
  ../../etc/x escaped the staging tree. Add a resolveUnder guard
  (mirrors the supervisor's) at both join sites; reject any path that
  resolves outside the target dir.

- Unbounded catalogue bundle download: the bundle tarball was copied
  with no size limit, so a hostile or compromised CDN could exhaust disk
  before the post-copy sha256 check ran. Cap the compressed download
  with io.LimitReader (maxBundleBytes) and fail closed when exceeded.

- Cap-state reads no longer fail open: loadCapStateRecords silently
  skipped malformed lines and tampered HMAC records, under-reporting
  usage and hiding an erased cap. It now returns an error and the caps
  command fails closed on a malformed line, an HMAC mismatch, or a
  signed chain spliced with an unauthenticated record.

Tests: ../../etc/x and absolute/empty paths rejected; oversized bundle
download refused; malformed line and tampered/mixed cap-state records
fail closed; legacy and nil-key reads still accepted.
Comment thread cmd/pilotctl/appstore.go Fixed
@TeoSlayer TeoSlayer merged commit ae2b6ba into main Jun 22, 2026
14 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.

3 participants