Skip to content

feat: Harden downloads: path traversal, size limits, and injection fixes - #40

Merged
liamg merged 3 commits into
mainfrom
security-audit-hardening
Jul 31, 2026
Merged

feat: Harden downloads: path traversal, size limits, and injection fixes#40
liamg merged 3 commits into
mainfrom
security-audit-hardening

Conversation

@liamg

@liamg liamg commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Remediations from a security audit of the download and extraction paths. All changes preserve existing behaviour for well-formed inputs; the new limits and restrictions are opt-in or set to unrestrictive defaults.

Fixes

S3 / GCS directory traversal (the one real write-outside bug). Directory downloads joined server-supplied object keys onto the temp dir with no containment check, so a bucket returning a key like ../../../home/you/.ssh/authorized_keys could write outside it. Both paths now go through a new internal/safepath.Join, which rejects any result that escapes the destination — while still allowing a .. that stays inside it (as requested, not a blanket .. ban). This matches the guard the tar/zip extractors and the git sparse path already had.

Vulnerable dependencies. Bumped golang.org/x/text 0.37.0 → 0.39.0 and the Go directive 1.25.2 → 1.26.5, clearing all three govulncheck findings reachable from this code (x/text infinite-loop DoS, crypto/tls ECH privacy leak, os symlink root-escape reachable via go-git clone). govulncheck ./... now reports no reachable vulnerabilities.

Decompression-bomb / unbounded-download DoS. New WithMaxDownloadBytes (default settings.DefaultMaxBytes = 10 GiB, 0 disables) bounds a single downloaded file and the total size of an archive extraction. Enforced in http, s3, gcs, the git archive fallback, and the archive extractor (cumulative across entries). Does not apply to OCI, which pulls through oras's own client.

Mercurial argument injection. hg clone received a URL-derived host positionally. A host beginning with - is now rejected, and -- is passed before the positional args, closing the ssh -oProxyCommand injection class (grabber no longer relies solely on hg's own protections).

SSRF via the connect probe. ProbeConnect dialled the target directly, bypassing the SSRF guard, so it could reach and reveal the reachability of loopback/link-local/private addresses the guarded fetch refuses. It now honours the guard (via a small Guard.Control / Guard.HostAllowed refactor).

setuid/setgid bits from untrusted archives. Archive entry modes are masked to permission bits only, so an archive can't land a setuid/setgid file on disk.

Arbitrary local reads via file::. New WithAllowedLocalDirectories(dirs ...) restricts the file protocol to a set of directories, resolving symlinks (including intermediate path components) so a link pointing out of an allowed tree is rejected. Empty by default (no restriction) — for callers that may be handed untrusted URLs.

Not changed (by design)

  • SSH host-key verification still "fails open" only in WithNoSystemFallback mode with no KnownHosts configured (opt-in; a changed key is still rejected once known_hosts is supplied).
  • md5/sha1 remain accepted for checksums (non-default, integrity not secrecy).

Tests

Added unit tests for safepath containment, limitio bounds, the extraction size budget (including cumulative-across-entries) and setuid stripping, the file allowed-dirs restriction (including symlink escape and a symlinked intermediate component), and the hg leading-dash rejection. Full suite passes (go test ./...); go vet and govulncheck clean.

liamg added 3 commits July 31, 2026 13:04
Remediations from a security audit of the download and extraction paths.

- s3/gcs: constrain directory downloads to the destination directory.
  Object keys come from the bucket listing (attacker-influenced) and were
  joined onto tmpDir with no containment check, so a key containing ".."
  could write outside it. Add internal/safepath and gate both writes on it.
  A ".." that stays inside the destination is still allowed.
- deps: bump golang.org/x/text 0.37.0 -> 0.39.0 and go 1.25.2 -> 1.26.5,
  clearing the three govulncheck findings reachable from this code
  (x/text DoS, crypto/tls ECH leak, os symlink root-escape).
- downloads/extraction: add WithMaxDownloadBytes (default 10 GiB) bounding a
  single downloaded file and the total size of an archive extraction, so a
  decompression bomb or unbounded body cannot exhaust the disk. Applied to
  http, s3, gcs, the git archive fallback, and the archive extractor.
- hg: reject a repo URL whose host begins with "-" and pass "--" before the
  positional args, closing the ssh -oProxyCommand argument-injection class in
  the hg subprocess.
- ssrf: apply the guard to the connect probe, which dialled the target
  directly and so could reach (and reveal the reachability of) internal
  addresses the guarded fetch refuses.
- extract: strip setuid/setgid/sticky bits from archive entry modes.
- file: add WithAllowedLocalDirectories to restrict file:// sources to a set
  of directories, resolving symlinks so a link out of an allowed tree is
  rejected. Useful when grabber may be handed untrusted URLs.
@liamg
liamg requested a review from liamcervante July 31, 2026 12:18
@liamg liamg changed the title Harden downloads: path traversal, size limits, and injection fixes chore: Harden downloads: path traversal, size limits, and injection fixes Jul 31, 2026
@liamg liamg changed the title chore: Harden downloads: path traversal, size limits, and injection fixes feat: Harden downloads: path traversal, size limits, and injection fixes Jul 31, 2026
@liamg
liamg merged commit 26bbe14 into main Jul 31, 2026
3 checks passed
@liamg
liamg deleted the security-audit-hardening branch July 31, 2026 13:02
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.

2 participants