Skip to content

fix(security): require https origins and refuse a prefix-resident cosign - #792

Open
rustytrees wants to merge 1 commit into
indaco:mainfrom
rustytrees:fix/require-https-and-trusted-cosign
Open

fix(security): require https origins and refuse a prefix-resident cosign#792
rustytrees wants to merge 1 commit into
indaco:mainfrom
rustytrees:fix/require-https-and-trusted-cosign

Conversation

@rustytrees

Copy link
Copy Markdown

Third slice of #789 — the integrity gaps on the trust path. None of these needed a bug to exploit; they follow from a manifest or a package doing what it is already allowed to do.

Cleartext origins are refused

The redirect handling in net/client.zig is careful: credentials are dropped on a cross-domain hop and an https→http downgrade mid-chain is refused outright. But the initial scheme came straight from a manifest. A tap that writes http:// for a formula or cask URL got a plaintext fetch — and since a cask may legitimately carry no sha256, on that path there can be nothing but the transport standing between the manifest and an installed artifact.

Every entry point (get, getWithHeaders, getToWriter, head) now refuses a cleartext origin. cli/tap.zig already rejects http:// when registering a tap; this is the same rule applied to the URLs those taps hand back.

Loopback (127.0.0.1, localhost, ::1) is exempt so the fixture-server tests keep working. That felt better than an env-var opt-out, which a real deployment could trip over — and the tests pin that the carve-out is a host match, not a substring, so http://evil.tld/127.0.0.1/… and http://127.0.0.1.evil.tld/… are both still refused.

cosign is not trusted from inside the prefix

cosign is resolved off PATH, which includes <prefix>/bin — a directory malt installs packages into. A shim landing there turns every later mt version update into a rubber stamp, because the only signal this module reads back is the child's exit status.

The lookup now mirrors execvp (first PATH hit wins), resolves the candidate, and refuses one that lands inside the prefix. Both sides are realpath'd: comparing a resolved binary against an unresolved prefix is spelling-sensitive — on macOS /tmp is a symlink to /private/tmp — which the test caught before this went anywhere.

New CosignUntrusted error with its own message, rather than folding into CosignNotFound; "install cosign" would be the wrong advice when cosign is present but in the wrong place. Zig's exhaustive switches pointed at both call sites that had to decide.

install.sh checksum lookup

grep "$ARCHIVE_NAME" treated the filename as a regex and matched anywhere on the line, so a longer name containing this one would also hit and make EXPECTED multi-line. checksums.txt is cosign-verified before this runs, so it was never exploitable — it just should not depend on that ordering. Now a fixed-string match anchored on the two-space GoReleaser separator.

Verification

  • zig build test — full suite green
  • ./scripts/test/install_sh_test.sh — 13/13 (covers the changed line)
  • ./scripts/lint-spawn-invariants.sh — clean
  • ./scripts/smokes/smoke_security.sh — 8/8

Three integrity gaps on the trust path, none of which needed a bug to
exploit — just a manifest or a package doing what it is already allowed to.

**Cleartext origins.** The redirect loop already declines an https→http
downgrade mid-chain, but the *initial* scheme came straight from a manifest.
A tap that writes `http://` for a formula or cask URL got a plaintext fetch,
and a cask may legitimately carry no `sha256`, leaving nothing but the
transport to substitute against. Every entry point now refuses a cleartext
origin. Loopback is exempt so the fixture-server tests keep working without
an escape hatch a real deployment could trip over — `cli/tap.zig` already
refuses `http://` when registering a tap, so this is that rule applied to the
URLs those taps hand back.

**cosign resolved from the prefix.** `cosign` is looked up on PATH, which
includes `<prefix>/bin` — a directory malt installs packages into. A shim
landing there turns every later `mt version update` into a rubber stamp,
because the only signal read back is the child's exit status. The lookup now
mirrors execvp, resolves both sides, and refuses a verifier inside the
prefix. Both paths are realpath'd: comparing a resolved binary against an
unresolved prefix is spelling-sensitive (on macOS `/tmp` is a symlink to
`/private/tmp`), which the test caught.

**install.sh checksum lookup.** `grep "$ARCHIVE_NAME"` treated the name as a
regex and matched anywhere on the line, so a longer filename containing this
one also hit and made EXPECTED multi-line. checksums.txt is cosign-verified
first, so this was never exploitable — it just should not depend on that
ordering. Now a fixed-string match on the two-space GoReleaser separator.
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