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

Reply via email to