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 <siretart@tauware.de> 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?= <hony.com@seznam.cz>
+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 <hony.com@seznam.cz>
+(cherry picked from commit fc2003bb2efeb3ed7d5c7e1e88d04e78f370a944)
+Signed-off-by: Tom Sweeney <tsweeney@redhat.com>
+---
+ 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