Google: path traversal
Summary
The Cuttlefish Host Orchestrator exposes an unauthenticated artifact upload + extract API. A crafted .tar.gz or .zip writes files to arbitrary absolute paths outside the extraction directory, as the OS user the orchestrator runs as — a classic Zip-Slip/Tar-Slip plus symlink-following (tar). No credentials are required at the orchestrator layer.
Affected endpoints (no auth/authz middleware on either)
POST /v1/userartifacts/{checksum}— chunked uploadPOST /v1/userartifacts/{checksum}/:extract— extract the uploaded.tar.gz/.zip
Root cause
frontend/src/host_orchestrator/orchestrator/userartifacts.go joins the attacker-controlled archive entry name onto the destination directory with no containment check, and creates symlink entries verbatim:
```go
// untar()
target := filepath.Join(dst, header.Name) // attacker-controlled; not validated
switch header.Typeflag {
case tar.TypeReg:
f, _ := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) // writes anywhere
case tar.TypeSymlink:
os.Symlink(header.Linkname, target) // plants attacker-chosen symlink
}
// unzip()
extractTo(filepath.Join(dstDir, f.Name), f) // same missing check
`
No filepath.Clean+prefix check, filepath.Rel containment test, or ../absolute/symlink rejection anywhere on ExtractArtifact → extractFile → untar/unzip. So...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| github.com/google/android-cuttlefish/frontend/src/host_orchestrator Go | <= ac6214c7dd79567b6637bce478ecdcc653e028d5` | No fix yet |
Details and references
### Summary The Cuttlefish Host Orchestrator exposes an unauthenticated artifact upload + extract API. A crafted `.tar.gz` or `.zip` writes files to **arbitrary absolute paths outside the extraction directory**, as the OS user the orchestrator runs as — a classic Zip-Slip/Tar-Slip plus symlink-following (tar). No credentials are required at the orchestrator layer. ### Affected endpoints (no auth/authz middleware on either) - `POST /v1/userartifacts/{checksum}` — chunked upload - `POST /v1/userartifacts/{checksum}/:extract` — extract the uploaded `.tar.gz` / `.zip` ### Root cause `frontend/src/host_orchestrator/orchestrator/userartifacts.go` joins the attacker-controlled archive entry name onto the destination directory with **no containment check**, and creates symlink entries verbatim: ```go // untar() target := filepath.Join(dst, header.Name) // attacker-controlled; not validated switch header.Typeflag { case tar.TypeReg: f, _ := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) // writes anywhere case tar.TypeSymlink: os.Symlink(header.Linkname, target) // plants attacker-chosen symlink } // unzip() extractTo(filepath.Join(dstDir, f.Name), f) // same missing check ``` No `filepath.Clean`+prefix check, `filepath.Rel` containment test, or `..`/absolute/symlink rejection anywhere on `ExtractArtifact → extractFile → untar`/`unzip`. So `../../../../etc/cron.d/x` escapes the root (CWE-22), and a symlink entry (`sneak → /target`) followed by `sneak/file` writes through it to an arbitrary absolute path (CWE-59). ### Proof of concept A benign, deterministic Go test drives the real exported `ExtractArtifact()` over an artifact staged as the upload handler leaves it on disk, and asserts canary files escape the extraction root (all three vectors), exit 0. (Attach `userartifacts_zipslip_poc_test.go` + `poc_run.txt`.) ``` git clone https://github.com/google/android-cuttlefish cd android-cuttlefish && git checkout ac6214c7dd79567b6637bce478ecdcc653e028d5 cd frontend/src/host_orchestrator # drop userartifacts_zipslip_poc_test.go into orchestrator/ GOWORK=off go test ./orchestrator/ -run ZipSlipPoC -v ``` ### Impact Unauthenticated arbitrary file write → RCE (write `~/.ssh/authorized_keys`, a cron/systemd unit, or overwrite a Cuttlefish binary/config the orchestrator later executes). In Cuttlefish-as-a-Service — orchestrator reachable behind the operator / `cvd-remote`, processing user artifacts — this is a remote, unauthenticated, multi-tenant host-compromise primitive. ### Suggested fix (apply to `untar` and `unzip`) ```go rel, err := filepath.Rel(dst, target) if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { return fmt.Errorf("archive entry escapes destination: %q", name) } // and skip tar.TypeSymlink entries (or verify the resolved link target stays within dst) ```