Control: tags -1 - moreinfo Hi Jonathan, Salvatore,
My apologies for the long delay in following up, and for omitting the debdiff in the initial submission. Please find the debdiff included below. Regarding podman: A binary rebuild (binNMU) is sufficient. The security fix does not alter any public APIs or exported interfaces in Buildah, so rebuilding podman against the updated golang-github-containers-buildah-dev pulls in the fix without requiring any source changes. Thanks, Reinhard --- debdiff --- diff --git golang-github-containers-buildah-1.39.3+ds1/debian/changelog golang-github-containers-buildah-1.39.3+ds1/debian/changelog index a2ba2201a..6c25b5f98 100644 --- golang-github-containers-buildah-1.39.3+ds1/debian/changelog +++ golang-github-containers-buildah-1.39.3+ds1/debian/changelog @@ -1,3 +1,11 @@ +golang-github-containers-buildah (1.39.3+ds1-1+deb13u1) trixie; urgency=high + + * Backport upstream fix for CVE-2026-44517: symlink-based path traversal + in build contexts (ADD/COPY with malicious Git repos or tar archives) + Closes: #1140619 + + -- Reinhard Tartler <[email protected]> Sun, 28 Jun 2026 13:16:49 -0400 + golang-github-containers-buildah (1.39.3+ds1-1) unstable; urgency=medium * New upstream release diff --git golang-github-containers-buildah-1.39.3+ds1/debian/patches/0004-integration-tests-Prefer-ubi8-over-ubi9-image.patch golang-github-containers-buildah-1.39.3+ds1/debian/patches/0004-integration-tests-Prefer-ubi8-over-ubi9-image.patch index 3587bd03f..bcd9040ae 100644 --- golang-github-containers-buildah-1.39.3+ds1/debian/patches/0004-integration-tests-Prefer-ubi8-over-ubi9-image.patch +++ golang-github-containers-buildah-1.39.3+ds1/debian/patches/0004-integration-tests-Prefer-ubi8-over-ubi9-image.patch @@ -11,7 +11,7 @@ Using an older version of rhel works equially fine without this requirement. 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/chroot.bats b/tests/chroot.bats -index 8df3519..47fafe1 100644 +index 1826a5f..e893951 100644 --- a/tests/chroot.bats +++ b/tests/chroot.bats @@ -22,7 +22,7 @@ load helpers diff --git golang-github-containers-buildah-1.39.3+ds1/debian/patches/0005-Disable-TestDeviceFromPath.patch golang-github-containers-buildah-1.39.3+ds1/debian/patches/0005-Disable-TestDeviceFromPath.patch index 2733386ba..f0518c70a 100644 --- golang-github-containers-buildah-1.39.3+ds1/debian/patches/0005-Disable-TestDeviceFromPath.patch +++ golang-github-containers-buildah-1.39.3+ds1/debian/patches/0005-Disable-TestDeviceFromPath.patch @@ -8,7 +8,7 @@ Closes: #1072147 1 file changed, 1 insertion(+) diff --git a/pkg/parse/parse_test.go b/pkg/parse/parse_test.go -index 6cbb3af..94881b4 100644 +index aeb842f..56186d9 100644 --- a/pkg/parse/parse_test.go +++ b/pkg/parse/parse_test.go @@ -88,6 +88,7 @@ func TestIsValidDeviceMode(t *testing.T) { diff --git golang-github-containers-buildah-1.39.3+ds1/debian/patches/0007-Prevent-symlink-based-path-traversal-in-build-contex.patch golang-github-containers-buildah-1.39.3+ds1/debian/patches/0007-Prevent-symlink-based-path-traversal-in-build-contex.patch new file mode 100644 index 000000000..e6e72c336 --- /dev/null +++ golang-github-containers-buildah-1.39.3+ds1/debian/patches/0007-Prevent-symlink-based-path-traversal-in-build-contex.patch @@ -0,0 +1,376 @@ +From: =?utf-8?q?Jan_Rod=C3=A1k?= <[email protected]> +Date: Thu, 14 May 2026 17:59:47 +0200 +Subject: Prevent symlink-based path traversal in build contexts +MIME-Version: 1.0 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 8bit + +Use securejoin.SecureJoin in TempDirForURL to prevent symlink escape in +subdirectory resolution. Use os.OpenRoot in downloadToDirectory and +stdinToDirectory to safely write the Dockerfile fallback without +following symlinks left by partial tar extractions. + +Fixes: https://github.com/containers/buildah/security/advisories/GHSA-49p4-px3h-rq49 +Fixes: CVE-2026-44517 + +Signed-off-by: Jan Rodák <[email protected]> +(cherry picked from commit fc2003bb2efeb3ed7d5c7e1e88d04e78f370a944) +Signed-off-by: Tom Sweeney <[email protected]> +--- + define/types.go | 71 ++++++++++++++++++++++++++------- + define/types_test.go | 75 +++++++++++++++++++++++++++++++++++ + tests/NEW-IMAGES | 2 + + tests/bud.bats | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++ + 4 files changed, 242 insertions(+), 15 deletions(-) + +diff --git a/define/types.go b/define/types.go +index d2a820b..c45eac4 100644 +--- a/define/types.go ++++ b/define/types.go +@@ -14,10 +14,10 @@ import ( + "path/filepath" + "strings" + ++ securejoin "github.com/cyphar/filepath-securejoin" + "github.com/containers/image/v5/manifest" + "github.com/containers/storage/pkg/archive" + "github.com/containers/storage/pkg/chrootarchive" +- "github.com/containers/storage/pkg/ioutils" + "github.com/containers/storage/types" + v1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/opencontainers/runtime-spec/specs-go" +@@ -204,8 +204,16 @@ func TempDirForURL(dir, prefix, url string) (name string, subdir string, err err + } + return "", "", fmt.Errorf("cloning %q to %q:\n%s: %w", url, name, string(combinedOutput), err) + } +- logrus.Debugf("Build context is at %q", filepath.Join(downloadDir, gitSubDir)) +- return name, filepath.Join(filepath.Base(downloadDir), gitSubDir), nil ++ absPath, err := securejoin.SecureJoin(downloadDir, gitSubDir) ++ if err != nil { ++ return "", "", err ++ } ++ subdir, err := filepath.Rel(name, absPath) ++ if err != nil { ++ return "", "", err ++ } ++ logrus.Debugf("Build context is at %q", subdir) ++ return name, subdir, nil + } + if strings.HasPrefix(url, "github.com/") { + ghurl := url +@@ -221,8 +229,16 @@ func TempDirForURL(dir, prefix, url string) (name string, subdir string, err err + } + return "", "", err + } +- logrus.Debugf("Build context is at %q", filepath.Join(downloadDir, subdir)) +- return name, filepath.Join(filepath.Base(downloadDir), subdir), nil ++ absPath, err := securejoin.SecureJoin(downloadDir, subdir) ++ if err != nil { ++ return "", "", err ++ } ++ resultSubdir, err := filepath.Rel(name, absPath) ++ if err != nil { ++ return "", "", err ++ } ++ logrus.Debugf("Build context is at %q", resultSubdir) ++ return name, resultSubdir, nil + } + if url == "-" { + err = stdinToDirectory(downloadDir) +@@ -232,8 +248,8 @@ func TempDirForURL(dir, prefix, url string) (name string, subdir string, err err + } + return "", "", err + } +- logrus.Debugf("Build context is at %q", filepath.Join(downloadDir, subdir)) +- return name, filepath.Join(filepath.Base(downloadDir), subdir), nil ++ logrus.Debugf("Build context is at %q", downloadDir) ++ return name, filepath.Base(downloadDir), nil + } + logrus.Debugf("don't know how to retrieve %q", url) + if err2 := os.RemoveAll(name); err2 != nil { +@@ -321,6 +337,8 @@ func downloadToDirectory(url, dir string) error { + if resp.ContentLength == 0 { + return fmt.Errorf("no contents in %q", url) + } ++ // Try to extract the response as a tar archive; if that fails, ++ // assume it is a raw Dockerfile and write it as such. + if err := chrootarchive.Untar(resp.Body, dir, nil); err != nil { + resp1, err := http.Get(url) + if err != nil { +@@ -331,10 +349,8 @@ func downloadToDirectory(url, dir string) error { + if err != nil { + return err + } +- dockerfile := filepath.Join(dir, "Dockerfile") +- // Assume this is a Dockerfile +- if err := ioutils.AtomicWriteFile(dockerfile, body, 0o600); err != nil { +- return fmt.Errorf("failed to write %q to %q: %w", url, dockerfile, err) ++ if err := writeFileInRoot(dir, "Dockerfile", body, 0o600); err != nil { ++ return fmt.Errorf("failed to write %q to %q: %w", url, filepath.Join(dir, "Dockerfile"), err) + } + } + return nil +@@ -347,13 +363,38 @@ func stdinToDirectory(dir string) error { + if err != nil { + return fmt.Errorf("failed to read from stdin: %w", err) + } ++ // Try to extract the buffered input as a tar archive; if that fails, ++ // assume it is a raw Dockerfile and write it as such. + reader := bytes.NewReader(b) + if err := chrootarchive.Untar(reader, dir, nil); err != nil { +- dockerfile := filepath.Join(dir, "Dockerfile") +- // Assume this is a Dockerfile +- if err := ioutils.AtomicWriteFile(dockerfile, b, 0o600); err != nil { +- return fmt.Errorf("failed to write bytes to %q: %w", dockerfile, err) ++ if err := writeFileInRoot(dir, "Dockerfile", b, 0o600); err != nil { ++ return fmt.Errorf("failed to write bytes to %q: %w", filepath.Join(dir, "Dockerfile"), err) + } + } + return nil + } ++ ++// writeFileInRoot safely writes data to a file inside root, without following ++// symlinks that escape the root directory. ++func writeFileInRoot(root, name string, data []byte, perm os.FileMode) error { ++ rootHandle, err := os.OpenRoot(root) ++ if err != nil { ++ return err ++ } ++ defer rootHandle.Close() ++ ++ if err := rootHandle.Remove(name); err != nil && !errors.Is(err, os.ErrNotExist) { ++ return err ++ } ++ ++ fileHandle, err := rootHandle.OpenFile(name, os.O_CREATE|os.O_EXCL|os.O_WRONLY, perm) ++ if err != nil { ++ return err ++ } ++ ++ _, err = fileHandle.Write(data) ++ if closeErr := fileHandle.Close(); closeErr != nil && err == nil { ++ err = closeErr ++ } ++ return err ++} +diff --git a/define/types_test.go b/define/types_test.go +index 9ca2fa6..12438e8 100644 +--- a/define/types_test.go ++++ b/define/types_test.go +@@ -1,11 +1,86 @@ + package define + + import ( ++ "os" ++ "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" ++ "github.com/stretchr/testify/require" + ) + ++func TestWriteFileInRoot(t *testing.T) { ++ t.Parallel() ++ ++ t.Run("creates file normally", func(t *testing.T) { ++ t.Parallel() ++ root := t.TempDir() ++ err := writeFileInRoot(root, "Dockerfile", []byte("FROM scratch\n"), 0o600) ++ require.NoError(t, err) ++ content, err := os.ReadFile(filepath.Join(root, "Dockerfile")) ++ require.NoError(t, err) ++ assert.Equal(t, "FROM scratch\n", string(content)) ++ }) ++ ++ t.Run("overwrites existing regular file", func(t *testing.T) { ++ t.Parallel() ++ root := t.TempDir() ++ err := os.WriteFile(filepath.Join(root, "Dockerfile"), []byte("old"), 0o600) ++ require.NoError(t, err) ++ err = writeFileInRoot(root, "Dockerfile", []byte("new"), 0o600) ++ require.NoError(t, err) ++ content, err := os.ReadFile(filepath.Join(root, "Dockerfile")) ++ require.NoError(t, err) ++ assert.Equal(t, "new", string(content)) ++ }) ++ ++ t.Run("does not follow symlink escaping root", func(t *testing.T) { ++ t.Parallel() ++ root := t.TempDir() ++ ++ targetDir := t.TempDir() ++ target := filepath.Join(targetDir, "pwned") ++ err := os.WriteFile(target, []byte("original"), 0o600) ++ require.NoError(t, err) ++ ++ err = os.Symlink(target, filepath.Join(root, "Dockerfile")) ++ require.NoError(t, err) ++ ++ err = writeFileInRoot(root, "Dockerfile", []byte("attacker content"), 0o600) ++ require.NoError(t, err) ++ ++ content, err := os.ReadFile(target) ++ require.NoError(t, err) ++ assert.Equal(t, "original", string(content)) ++ ++ info, err := os.Lstat(filepath.Join(root, "Dockerfile")) ++ require.NoError(t, err) ++ assert.True(t, info.Mode().IsRegular()) ++ }) ++ ++ t.Run("does not follow relative symlink escaping root", func(t *testing.T) { ++ t.Parallel() ++ // Create structure: parentDir/root/ and parentDir/secret ++ parentDir := t.TempDir() ++ root := filepath.Join(parentDir, "root") ++ err := os.Mkdir(root, 0o755) ++ require.NoError(t, err) ++ secret := filepath.Join(parentDir, "secret") ++ err = os.WriteFile(secret, []byte("sensitive data"), 0o600) ++ require.NoError(t, err) ++ ++ err = os.Symlink("../secret", filepath.Join(root, "Dockerfile")) ++ require.NoError(t, err) ++ ++ err = writeFileInRoot(root, "Dockerfile", []byte("attacker content"), 0o600) ++ require.NoError(t, err) ++ ++ content, err := os.ReadFile(secret) ++ require.NoError(t, err) ++ assert.Equal(t, "sensitive data", string(content)) ++ }) ++} ++ + func TestParseGitBuildContext(t *testing.T) { + // Tests with only repo + repo, subdir, branch := parseGitBuildContext("https://github.com/containers/repo.git") +diff --git a/tests/NEW-IMAGES b/tests/NEW-IMAGES +index 2096017..f6af0ca 100644 +--- a/tests/NEW-IMAGES ++++ b/tests/NEW-IMAGES +@@ -13,3 +13,5 @@ + # + # Format is one FQIN per line. Enumerate them below: + # ++registry.access.redhat.com/ubi10 ++quay.io/hummingbird/git +diff --git a/tests/bud.bats b/tests/bud.bats +index 10e4f81..747a1b1 100644 +--- a/tests/bud.bats ++++ b/tests/bud.bats +@@ -7228,6 +7228,115 @@ _EOF + assert "$output" = "$local_head_hash" + } + ++@test "bud with ADD with git repository source escape directory" { ++ _prefetch alpine ++ ++ local secretdir=${TEST_SCRATCH_DIR}/secretdir ++ mkdir -p ${secretdir} ++ echo mysecret > ${secretdir}/secretfile ++ ++ local repodir=${TEST_SCRATCH_DIR}/repo ++ mkdir -p ${repodir} ++ git -C ${repodir} init -b main ++ ln -s / ${repodir}/proj ++ git -C ${repodir} add proj ++ git -C ${repodir} commit -m "initial commit" ++ ++ local baredir=${TEST_SCRATCH_DIR}/repository ++ mkdir -p ${baredir} ++ git clone --bare ${repodir} ${baredir}/test-bug.git ++ ++ starthttpd /git/=${baredir}:"git http-backend":GIT_HTTP_EXPORT_ALL=1:GIT_PROJECT_ROOT=${baredir} ${baredir} ++ ++ local contextdir=${TEST_SCRATCH_DIR}/add-git ++ mkdir -p $contextdir ++ cat > $contextdir/Containerfile << _EOF ++FROM alpine ++ADD http://0.0.0.0:${HTTP_SERVER_PORT}/git/test-bug.git#main:proj${secretdir} /mydir ++RUN cat /mydir/secretfile ++_EOF ++ ++ run_buildah 125 build -f $contextdir/Containerfile -t escape-image --no-cache $contextdir ++ assert "$output" !~ "mysecret" ++} ++ ++@test "bud with http context symlinked Dockerfile does not write through symlink on fallback" { ++ local targetfile=${TEST_SCRATCH_DIR}/targetfile ++ echo "ORIGINAL_CONTENT" > ${targetfile} ++ ++ # Create a tar with a symlink Dockerfile pointing to targetfile, then ++ # append garbage so chrootarchive.Untar creates the symlink but fails. ++ local tarsrc=${TEST_SCRATCH_DIR}/tarsrc ++ mkdir -p ${tarsrc} ++ ln -s ${targetfile} ${tarsrc}/Dockerfile ++ local broken_tar=${TEST_SCRATCH_DIR}/broken.tar ++ tar -cf ${broken_tar} -C ${tarsrc} Dockerfile ++ dd if=/dev/urandom bs=512 count=4 >> ${broken_tar} 2>/dev/null ++ ++ # Start a Python HTTP server that serves the broken tar on the first ++ # request (triggers Untar failure + symlink creation), then a malicious ++ # Dockerfile on the second request (which would be written through the ++ # symlink without the fix). ++ local portfile=${TEST_SCRATCH_DIR}/port ++ python3 -c " ++import http.server, threading, sys ++ ++class Handler(http.server.BaseHTTPRequestHandler): ++ count = 0 ++ def do_GET(self): ++ Handler.count += 1 ++ self.send_response(200) ++ self.end_headers() ++ if Handler.count % 2 == 1: ++ with open('${broken_tar}', 'rb') as f: ++ self.wfile.write(f.read()) ++ else: ++ self.wfile.write(b'FROM scratch\nRUN echo MALICIOUS_CONTENT\n') ++ def log_message(self, *args): ++ pass ++ ++srv = http.server.HTTPServer(('0.0.0.0', 0), Handler) ++with open('${portfile}', 'w') as f: ++ f.write(str(srv.server_address[1])) ++srv.serve_forever() ++" & ++ local srv_pid=$! ++ # Wait for the server to write its port. ++ local waited=0 ++ while ! test -s ${portfile}; do ++ sleep 0.1 ++ if test $((++waited)) -ge 50; then ++ kill $srv_pid 2>/dev/null || true ++ die "python http server did not start" ++ fi ++ done ++ local port=$(< ${portfile}) ++ ++ run_buildah 125 build $WITH_POLICY_JSON http://0.0.0.0:${port}/context.tar ++ kill $srv_pid 2>/dev/null || true ++ ++ run cat ${targetfile} ++ assert "$output" = "ORIGINAL_CONTENT" ++} ++ ++@test "bud with stdin context symlinked Dockerfile does not write through symlink on fallback" { ++ local targetfile=${TEST_SCRATCH_DIR}/targetfile ++ echo "ORIGINAL_CONTENT" > ${targetfile} ++ ++ # Create a tar with a symlink Dockerfile pointing to targetfile, then ++ # append garbage so chrootarchive.Untar fails after creating the symlink. ++ local tarsrc=${TEST_SCRATCH_DIR}/tarsrc ++ mkdir -p ${tarsrc} ++ ln -s ${targetfile} ${tarsrc}/Dockerfile ++ local broken_tar=${TEST_SCRATCH_DIR}/broken.tar ++ tar -cf ${broken_tar} -C ${tarsrc} Dockerfile ++ dd if=/dev/urandom bs=512 count=4 >> ${broken_tar} 2>/dev/null ++ ++ run_buildah 125 build $WITH_POLICY_JSON - < ${broken_tar} ++ run cat ${targetfile} ++ assert "$output" = "ORIGINAL_CONTENT" ++} ++ + @test "build-validates-bind-bind-propagation" { + _prefetch alpine + diff --git golang-github-containers-buildah-1.39.3+ds1/debian/patches/series golang-github-containers-buildah-1.39.3+ds1/debian/patches/series index 5c4c9cd23..5402e3e48 100644 --- golang-github-containers-buildah-1.39.3+ds1/debian/patches/series +++ golang-github-containers-buildah-1.39.3+ds1/debian/patches/series @@ -2,3 +2,4 @@ root-testfail-ignore.patch 0005-Disable-TestDeviceFromPath.patch 0006-tolerate-absence-of-netavark-binary-in-tests.patch 0004-integration-tests-Prefer-ubi8-over-ubi9-image.patch +0007-Prevent-symlink-based-path-traversal-in-build-contex.patch

