Path confinement for `secrets.file` and `configs.file` in stack deploy
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 70/100
Direzione di ricerca
Inizia in cli/compose/loader/loader.go, in loadFileObjectConfig(), quindi segui il percorso risolto fino a os.ReadFile() in cli/compose/convert/compose.go. Riproduci il percorso con docker stack deploy ed esercita sia secrets.file sia configs.file. Il lavoro è completato quando i percorsi al di fuori della directory del progetto, inclusi i percorsi assoluti, vengono rifiutati, mentre i file all’interno del progetto continuano a essere distribuiti normalmente.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Description
We've found a path confinement issue in docker cli. Documentation is unclear if path confinement is expected, but I did want to raise as a security issue first.
Also I’ve been watching the push to harden docker including the new docker sandboxes so I am inclined to think that if this is not currently a vulnerability it will be considered so in the future.
Specifically, when docker stack deploy processes a compose file, secrets.file and configs.file paths are resolved relative to the project directory using filepath.Join, but the resolved path is not checked for confinement within that directory. A value like file: "../../etc/shadow" cleanly resolves to /etc/shadow, and the CLI reads the file and uploads its contents to the Swarm secret store. Absolute paths work too. This is handled by loadFileObjectConfig() in cli/compose/loader/loader.go:672, which calls absPath(), and the result is passed directly to os.ReadFile() in cli/compose/convert/compose.go:185.
We've confirmed this end-to-end against the current docker/cli source using a Go test that exercises the actual loader and converter. The canary file outside the project directory is read and packaged as a Swarm secret.
We recognize this is consistent with how Compose resolves relative paths today, and the documentation describes these paths as "relative to" the project directory without promising confinement. We're not framing this as a contract violation. However, secret and config file reads are unique among Compose path features because the file content leaves the operator's host machine. It's uploaded to the Swarm secret store and distributed to service containers. This is a data exfiltration path that doesn't exist with bind mounts (which give the container access to a host path but don't copy content off the host) or env_file (which stays local to the CLI process). An operator deploying a compose file from a shared repo, PR, or quickstart guide may not expect that a secrets.file directive causes reads outside the project tree and transmits the result to Swarm.
Given Docker's broader investment in hardened defaults like microVMs, build attestations, and content trust, we think confining secrets.file and configs.file resolution to the project directory would be a low-cost improvement that aligns with that direction. The docker/compose project added similar path confinement in the git loader (PR #13331) for content from untrusted sources; the same principle applies when the compose file itself is the untrusted input. The fix is approximately five lines at a single call site in loadFileObjectConfig() — a filepath.Clean plus a strings.HasPrefix check after the existing absPath call. It would not affect volume bind mounts or env_file, which have different documented semantics. We have a working POC and suggested patch available if helpful.
Steps to reproduce
- Create a canary file outside the project directory
echo “MY SECRET" > /tmp/secret.txt```
Change /tmp/secret.txt to whatever works for you...
2. Create a compose file that traverses out of the project dir
Contents of the file if stored as /tmp/poc/test.yml (but you can change the file: to whatever makes sense for your path:
version: "3.8"
secrets:
leaked:
file: "../secret.txt"
services:
app:
image: alpine
secrets: [leaked]
command: ["cat", "/run/secrets/leaked"]
3. Deploy — the CLI reads /tmp/poc/secret.txt and uploads it as a Swarm secret
docker swarm init # if not already in swarm mode
docker stack deploy -c /tmp/poc/test.yml poc-test
# 4. Verify the secret was created with the canary content
docker service logs poc-test_app
# Output: MY SECRET
# Cleanup
docker stack rm poc-test
docker swarm leave --force
The traversal also works with absolute paths (file: "/etc/passwd") and deeper relative paths (file: "../../../../home/user/.ssh/id_rsa"). A Go unit test exercising the actual loader and converter code is available on request.
Here’s the proof:
PastedGraphic-1.png
Suggested fix
Add a confinement check in loadFileObjectConfig after the absPath call.
This is the only call site that needs it — env_file and volume bind mounts
have different documented semantics.
// cli/compose/loader/loader.go — inside loadFileObjectConfig, after line 672
default:
obj.File = absPath(details.WorkingDir, obj.File)
// Verify resolved path stays within the project directory
cleanBase := filepath.Clean(details.WorkingDir) + string(filepath.Separator)
cleanFile := filepath.Clean(obj.File)
if !strings.HasPrefix(cleanFile+string(filepath.Separator), cleanBase) {
return obj, fmt.Errorf(
"%s %s: file path %q resolves to %q, which is outside the project directory",
objType, name, obj.File, cleanFile,
)
}
This is ~5 lines at a single call site. It covers both secrets and configs
since both go through loadFileObjectConfig.
- Lingua principale
- Go
- Stelle
- 6.1k
- Fork
- 2.2k
- Merge medio
- 1g 15h
- PR unite (30g)
- 43
Guida per i contributori
Apri la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di docker/cli
-
kind/bug status/0-triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
-
kind/bug status/0-triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
-
kind/bug status/0-triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 63/100
-
area/ux kind/enhancement
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
Issue simili
-
kind/bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
kubernetes-sigs/prow#953 · 1 commento ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
caddyserver/caddy#8046 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100
-
L1 recommended for recruits
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
-
area/entangle bug
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 92/100