Skip to content

Sandbox containers[].files[].source to the Score file's directory #495

@Siddhartha-singh01

Description

@Siddhartha-singh01

Describe the bug

A Score file's containers[].files[].source field is read via os.ReadFile in internal/compose/convert.go:313 with no sandboxing on the resolved path. Absolute paths pass through unchanged; relative paths are joined to the Score file's directory using filepath.Join, which does not constrain .. segments. A Score file can therefore read any host file the running user can access (e.g. source: /etc/passwd or source: ../../../some/host/file), with the contents landing in <mountsDir>/files/ and bind-mounted into the container.

To Reproduce

  1. In an empty directory, run score-compose init.
  2. Create a score.yaml (shown below) declaring a container with a files entry pointing at a host file outside the project directory.
  3. Run score-compose generate score.yaml.
  4. Observe that .score-compose/mounts/files/leak-files-leaked contains the contents of /etc/hostname, and the generated compose.yaml bind-mounts it into the container.

score.yaml:

apiVersion: score.dev/v1b1
metadata:
  name: leak
containers:
  web:
    image: alpine
    files:
      "/leaked":
        source: "/etc/hostname"

A relative source: "../../../etc/hostname" demonstrates the same behavior filepath.Join does not reject the escape.

Expected behavior

A source path that resolves outside the Score file's directory should be rejected with a clear error, the same way internal/provisioners/core.go:208 already rejects non-local provisioner-generated paths using filepath.IsLocal.

Screenshots

N/A.

Desktop

  • OS: any (the affected code path is platform-independent Go)
  • Version: reproducible on main (current HEAD)

Additional context

The codebase already uses the correct sandboxing primitive in one place but not the other:

  • internal/provisioners/core.go:208 rejects non-local paths from provisioner outputs with filepath.IsLocal.
  • internal/compose/convert.go:313 does not apply the same check to user-supplied files[].source paths.

Threat model:

  • Local single-developer use: not affected the developer authors their own Score file.
  • CI workflows running score-compose generate on Score files from PR forks: contributor-supplied Score files can exfiltrate any file the runner can read into the mounts directory, which is often surfaced as a build artifact.
  • Embedded / multi-tenant use: same exposure.

Proposed fix in internal/compose/convert.go:

sourcePath := *file.Source
if !filepath.IsLocal(sourcePath) {
    return nil, fmt.Errorf("containers.%s.files[%s].source: must be a relative path within the Score file's directory", containerName, target)
}
if state.Workloads[workloadName].File != nil {
    sourcePath = filepath.Join(filepath.Dir(*state.Workloads[workloadName].File), sourcePath)
}
content, err = os.ReadFile(sourcePath)

filepath.IsLocal (Go 1.20+) rejects absolute paths, .. escapes, and volume-prefixed paths in a single call.

Discussed briefly with @mathieu-benoit on Slack, who suggested filing this as a public issue + PR rather than a security advisory.

Thanks!

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions