bazelbuild / bazelbuild/bazel

CompressedTarFunction: tar hard-link linknames with '..' escape the extraction destination (guard never fires for hard links)

Open
#30,875 0 comments 0 reactions 1 assignee Claimed by @meisterT View on GitHub
P2 team-ExternalDeps type: bug
Dominant language
Java
Stars
25.8k
Forks
4.6k
Avg merge
2d 20h
Merged PRs (30d)
72

Description

## Summary

`CompressedTarFunction` (used by all tar-based repository archive extraction — `http_archive`, `bazel_dep`, `repository_ctx.download_and_extract()` / `.extract()`) validates link-entry targets with a containment guard that **cannot fire for hard links**: for hard links, `maybeDeprefixSymlink` is invoked with `forceExtractRootRelative=true`, which always returns an absolute fragment (`root.getRelative(...).asFragment()`), while the guard at `CompressedTarFunction.java:141` is gated on `!targetName.isAbsolute()`. As a result, a hard-link entry whose relative linkname contains `..` resolves to a path **outside** the extraction destination, and `FileSystemUtils.createHardLink` (line 167) creates a link from inside the destination to an arbitrary existing file outside it. A following regular entry with the same name then truncates and writes **through** the hard link (line 171), overwriting the outside file with attacker-controlled content.

This happens at extraction time — before any BUILD/.bzl file from the archive is parsed or executed.

## Reproduction

tar.gz containing:
1. hard link entry `pwn` with linkname `../victim/precious.txt`
2. regular entry `pwn` with content `"PWNED-CONTENT"`

Extraction via the production `CompressedTarFunction.decompress()` (sources compiled verbatim from master @244855942, harness attached to the equivalent VRP report; the same construction also reproduces with plain `tar` semantics) returns normally and the victim file outside the destination is overwritten.

**Negative controls** — the identical escape attempt via a **symlink** entry and via a regular `../` member name are both correctly blocked by the existing guards:
- `Tar entries cannot refer to files outside of their directory`
- `path is escaping the destination directory`

So the containment boundary is clearly intended; only the hard-link path bypasses it. `CompressedTarFunctionTest` has dedicated escape tests for symlinks and regular files, but none for hard links. Prior hardening commits for sibling cases: `f942a706a3` ("Add path escape checks to decompression methods") and `9a5a518c15` ("Fix chained-symlink escape handling in CompressedTarFunction").

## Impact

A user merely builds a project whose dependency graph includes a malicious tarball (default flags, no opt-in). The archive content's author overwrites any existing file writable by the user on the same filesystem as the output base — e.g. `~/.bashrc`, `~/.gitconfig`, project sources outside the sandbox — before any code from the archive executes. Checksums (sha256) authenticate archive identity, not safety; the same class is a CVE in package managers (cf. node-tar GHSA-83g3-92jg-28cx).

## Suggested fix

Require containment on `resolvedTargetPath` for hard links before `createHardLink` (the same `startsWith(descriptor.destinationPath())` check the other paths already enforce), plus a hard-link-escape regression test. PR attached.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.