Google: path traversal
Critical9.8Published Aug 28, 2026
### 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)...
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) ```
More Google advisories
All Google| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| Aug 28 | Google Chrome: out-of-bounds read | High8.8 | 151.0.7922.72 |
| Aug 26 | Google langfun: insecure default | Critical9.2 | 0.1.2 |
| Aug 25 | Improper control of a resource through its lifetime in Workers in Google Chrome... | Low3.1 | 152.0.7977.65 |
| Aug 25 | Google Chrome: use after free | Critical9.6 | 152.0.7977.65 |
| Aug 25 | Google Chrome: information disclosure | Medium6.5 | 152.0.7977.65 |
| Aug 25 | Google Chrome: integer overflow | High8.3 | 152.0.7977.65 |