Skip to content

Harden extraction, attestation, and install orchestration#2

Merged
ascarter merged 4 commits into
mainfrom
ascarter/code-review-sustainability
Jun 6, 2026
Merged

Harden extraction, attestation, and install orchestration#2
ascarter merged 4 commits into
mainfrom
ascarter/code-review-sustainability

Conversation

@ascarter

@ascarter ascarter commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Sustainability code review of gh-tool with the resulting fixes implemented. Security, CI, and maintainability improvements across the extension.

All checks green locally: go build, go vet, staticcheck, gofmt, and go test -race ./.... Coverage improved (archive 75.9%→82.4%, tool 56.3%→60.1%).

Security (archive extraction — highest priority)

  • Tar symlink escape: reject symlinks whose target escapes the destination dir.
  • Path-traversal guard: replaced the weak strings.HasPrefix check with a separator-aware SafeJoin in a new shared internal/fsutil package (also used by tool).
  • Resource limits: cap total extracted bytes (2 GiB) and entry count (100k) to prevent decompression bombs.
  • .txz/.tlz bug: fixed the decompressed-name mapping.
  • Added adversarial extraction tests (traversal, symlink escape, oversized / too-many-entry archives).

Attestation

  • Distinguish no attestation published (warn, continue — the common case) from a genuine verification failure (an attestation exists but does not verify).
  • Added --require-attestation to make real failures abort, plus --no-verify / --require-attestation parity on upgrade.

Install orchestration

  • Eliminated the double gh release view per tool: the target tag is resolved once (in parallel for reconcile) and threaded into Install in both install-reconcile and upgrade.
  • Split isUpToDate into a pure, unit-tested upToDate comparison plus a resolveTargetTag helper, preserving failure semantics.

Maintainability

  • Added a minimal, overridable ghExec seam in internal/tool, making the latest-tag and attestation-verification paths unit-testable (new tests). The broader injectable gh-client interface was intentionally deferred to avoid an awkward Manager-only half-measure.
  • Unified command registration into each command's init().
  • Removed the unused, undocumented config.Settings struct.
  • Quoted interpolated paths in emitted zsh/fish shell integration.
  • Removed reinvented helpers and dead code; filepath.Walkfilepath.WalkDir.

CI

  • Hardened test.yml: OS matrix (linux/macos/windows), go vet, -race, a gofmt gate, and staticcheck.

Docs

  • Documented the new attestation flags in docs/commands.md.

🤖 Generated with Copilot CLI

ascarter and others added 4 commits June 6, 2026 16:06
Sustainability code review follow-through. Security, CI, and
maintainability fixes across the extension.

Security (archive extraction):
- Reject tar symlinks whose target escapes the destination dir.
- Replace the weak HasPrefix path guard with a separator-aware
  SafeJoin (new internal/fsutil package, shared with tool).
- Enforce extraction resource limits (max total bytes, max entries).
- Fix .txz/.tlz decompressed-name mapping.
- Add adversarial extraction tests (traversal, symlink escape,
  oversized/too-many-entry archives).

Attestation:
- Distinguish "no attestation published" (warn, continue) from a
  genuine verification failure (an attestation exists but does not
  verify). Add --require-attestation to make real failures abort.
- Add --no-verify and --require-attestation to upgrade for parity.

Install orchestration:
- Resolve each tool's target tag once and thread it into Install,
  eliminating the double `gh release view` per tool in both
  install-reconcile and upgrade. Resolve reconcile tags in parallel.
- Split isUpToDate into a pure upToDate comparison plus a
  resolveTargetTag helper; preserve failure semantics.

Maintainability / cleanup:
- Introduce an overridable ghExec seam so the latest-tag and
  attestation paths are unit-testable; add tests for both.
- Unify command registration in each command's init().
- Remove the unused, undocumented config.Settings struct.
- Quote interpolated paths in emitted zsh/fish shell integration.
- Replace reinvented helpers and dead code; Walk -> WalkDir.

CI:
- Add an OS matrix (linux/macos/windows), go vet, -race, a gofmt
  gate, and staticcheck to the test workflow.

Docs:
- Document the new attestation flags in docs/commands.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
gh-tool is a Unix-targeted CLI: installs are symlink-based, paths follow
the XDG layout, and shell integration covers only bash/zsh/fish. The
windows-latest matrix entry surfaced pre-existing Unix-only test
assumptions (forward-slash paths, exec bits, root-path handling) for a
runtime the tool does not actually support. Test on Linux and macOS, the
platforms gh-tool runs on, both with -race.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CodeQL's go/zipslip query flagged the tar and zip extractors because it
does not recognize the cross-package fsutil.SafeJoin helper as a
sanitizing barrier, so the archive entry name appeared to flow unchecked
into filesystem operations. The code was already safe, but the guard was
not legible to the scanner (or to a reader of the extractor).

Co-locate the canonical strings.HasPrefix-against-cleaned-destination
check directly with the file operations it protects, so the barrier
dominates the sink. Drop the now-unused SafeJoin and keep WithinDir as
the shared boundary primitive (still used by the symlink-escape check and
tool.pathWithin); add a WithinDir unit test.

Behavior is unchanged: the adversarial extraction tests (traversal and
symlink escape rejection) still pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CodeQL's go/zipslip query recognizes strings.HasPrefix(path, ...) as a
path-traversal sanitizer only on the branch where it returns true. The
previous guard folded the root-equality case into the condition
(target != cleanDest && !HasPrefix(...)), which left a path to the file
operations where HasPrefix was false, so CodeQL could not prove the
barrier dominated the sinks.

Skip the archive-root entry with a separate continue and rely solely on
the HasPrefix containment check, so reaching any extraction sink strictly
requires the check to pass. Behavior is unchanged; adversarial tests
still pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ascarter ascarter merged commit b88a51e into main Jun 6, 2026
6 checks passed
@ascarter ascarter deleted the ascarter/code-review-sustainability branch June 6, 2026 23:29
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.

1 participant