Path confinement for `secrets.file` and `configs.file` in stack deploy

Aperta Adatta ai principianti
#6,919 3 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
70/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Tranquilla
Stack tecnologico
docker, go
Ambito
cli, security

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

kind/feature status/0-triage
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

  1. 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

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di docker/cli

Tutte le issue di docker/cli

Issue simili

Altre issue su Go

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.