From 51c93e28440e637cdbf8e260abf2e3bb729ca761 Mon Sep 17 00:00:00 2001 From: adilburaksen Date: Mon, 1 Jun 2026 19:33:49 +0300 Subject: [PATCH] Validate download symlink targets stay within the output directory DownloadOutputs creates symlinks from output trees using the SymlinkTarget value returned by the server. The target was used verbatim, so a server could return a symlink whose target is an absolute path or uses '..' to point outside the output directory, creating links to arbitrary locations on the client filesystem. Reject symlink targets that resolve outside outDir before creating the link. This mirrors the containment already applied to output paths. --- go/pkg/client/cas_download.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/go/pkg/client/cas_download.go b/go/pkg/client/cas_download.go index c584ed8b..5bb608fa 100644 --- a/go/pkg/client/cas_download.go +++ b/go/pkg/client/cas_download.go @@ -9,6 +9,7 @@ import ( "path/filepath" "sort" "strconv" + "strings" "sync" "time" @@ -147,7 +148,20 @@ func (c *Client) DownloadOutputs(ctx context.Context, outs map[string]*TreeOutpu } } for _, out := range symlinks { - if err := os.Symlink(out.SymlinkTarget, filepath.Join(outDir, out.Path)); err != nil { + linkPath := filepath.Join(outDir, out.Path) + // out.SymlinkTarget is provided by the (untrusted) server. Reject + // targets that resolve outside outDir (absolute paths or ".." escapes) + // so that downloading a result cannot create links to arbitrary + // locations on the client filesystem. + resolvedTarget := out.SymlinkTarget + if !filepath.IsAbs(resolvedTarget) { + resolvedTarget = filepath.Join(filepath.Dir(linkPath), resolvedTarget) + } + if rel, err := filepath.Rel(outDir, resolvedTarget); err != nil || + rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return fullStats, fmt.Errorf("symlink target %q for %q escapes the output directory", out.SymlinkTarget, out.Path) + } + if err := os.Symlink(out.SymlinkTarget, linkPath); err != nil { return fullStats, err } }