VYPR
High severityNVD Advisory· Published Feb 27, 2026· Updated Mar 2, 2026

kaniko has tar archive path traversal in build context extraction allows writing files outside destination directory

CVE-2026-28406

Description

kaniko is a tool to build container images from a Dockerfile, inside a container or Kubernetes cluster. Starting in version 1.25.4 and prior to version 1.25.10, kaniko unpacks build context archives using filepath.Join(dest, cleanedName) without enforcing that the final path stays within dest. A tar entry like ../outside.txt escapes the extraction root and writes files outside the destination directory. In environments with registry authentication, this can be chained with docker credential helpers to achieve code execution within the executor process. Version 1.25.10 uses securejoin for path resolution in tar extraction.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Kaniko 1.25.4–1.25.9 has a path traversal in tar extraction that can be chained with credential helpers for code execution.

Vulnerability

Kaniko, a tool for building container images inside containers or Kubernetes clusters, contains a path traversal vulnerability in its tar extraction logic. Starting in version 1.25.4 and prior to version 1.25.10, the ExtractFile function uses filepath.Join(dest, cleanedName) without verifying that the resulting path remains within the intended destination directory. A tar entry with a name like ../outside.txt can escape the extraction root and write files to arbitrary locations on the host filesystem [1][3].

Exploitation

An attacker who can supply a malicious build context archive (e.g., via a Dockerfile build request) can craft tar entries with parent-directory references in tar entry names. No special authentication is required to trigger the extraction; the vulnerability is reachable whenever kaniko processes a user-provided archive. In environments where registry authentication is configured, the attacker can chain this file write with docker credential helpers to overwrite sensitive files, potentially achieving code execution within the executor process [3].

Impact

Successful exploitation allows an attacker arbitrary file write outside the intended extraction directory. When combined with credential helpers, this can lead to arbitrary code execution in the context of the kaniko executor. The vulnerability does not require a Docker daemon and affects all users of kaniko versions 1.25.4 through 1.25.9 [1][3].

Mitigation

Version 1.25.10 resolves the issue by using securejoin for path resolution during tar extraction, ensuring that all extracted paths are confined to the destination directory. The fix was implemented in pull request #326 and includes additional test cases for path traversal scenarios [2][4]. Users should upgrade to kaniko 1.25.10 or later. No workaround is available for earlier versions has been documented.

AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/chainguard-dev/kanikoGo
>= 1.25.4, < 1.25.101.25.10

Affected products

2
  • kaniko/kanikollm-create
    Range: >=1.25.4, <1.25.10
  • chainguard-forks/kanikov5
    Range: >= 1.25.4, < 1.25.10

Patches

1
a370e4b1f66e

fix(util): use securejoin for path resolution in tar extraction (#326)

https://github.com/chainguard-forks/kanikoTrevor DunlapFeb 27, 2026via ghsa
2 files changed · +301 4
  • pkg/util/fs_util.go+45 4 modified
    @@ -33,6 +33,7 @@ import (
     
     	"github.com/chainguard-dev/kaniko/pkg/config"
     	"github.com/chainguard-dev/kaniko/pkg/timing"
    +	securejoin "github.com/cyphar/filepath-securejoin"
     	"github.com/docker/docker/pkg/archive"
     	v1 "github.com/google/go-containerregistry/pkg/v1"
     	"github.com/karrick/godirwalk"
    @@ -189,12 +190,22 @@ func GetFSFromLayers(root string, layers []v1.Layer, opts ...FSOpt) ([]string, e
     			}
     
     			cleanedName := filepath.Clean(hdr.Name)
    +			if cleanedName == ".." || strings.HasPrefix(cleanedName, "../") {
    +				return nil, fmt.Errorf("tar entry %q is not allowed: references parent directory", hdr.Name)
    +			}
     			path := filepath.Join(root, cleanedName)
     			base := filepath.Base(path)
    -			dir := filepath.Dir(path)
     
     			if strings.HasPrefix(base, archive.WhiteoutPrefix) {
    -				logrus.Tracef("Whiting out %s", path)
    +				// SecureJoin resolves symlinks and ensures the result stays
    +				// within root, which is needed because this code path calls
    +				// os.RemoveAll.
    +				securePath, err := securejoin.SecureJoin(root, cleanedName)
    +				if err != nil {
    +					return nil, fmt.Errorf("resolving whiteout path for %q: %w", hdr.Name, err)
    +				}
    +				dir := filepath.Dir(securePath)
    +				logrus.Tracef("Whiting out %s", securePath)
     
     				name := strings.TrimPrefix(base, archive.WhiteoutPrefix)
     				path := filepath.Join(dir, name)
    @@ -300,7 +311,20 @@ func UnTar(r io.Reader, dest string) ([]string, error) {
     }
     
     func ExtractFile(dest string, hdr *tar.Header, cleanedName string, tr io.Reader) error {
    -	path := filepath.Join(dest, cleanedName)
    +	if cleanedName == ".." || strings.HasPrefix(cleanedName, "../") {
    +		return fmt.Errorf("tar entry %q is not allowed: references parent directory", hdr.Name)
    +	}
    +
    +	path, err := securejoin.SecureJoin(dest, cleanedName)
    +	if err != nil {
    +		// During layer extraction, symlink chains may be incomplete,
    +		// causing ELOOP. Fall back to the lexical path — the OS will
    +		// encounter the same resolution failure if the path is used.
    +		if !errors.Is(err, syscall.ELOOP) {
    +			return fmt.Errorf("resolving path for %q: %w", hdr.Name, err)
    +		}
    +		path = filepath.Join(dest, cleanedName)
    +	}
     	base := filepath.Base(path)
     	dir := filepath.Dir(path)
     	mode := hdr.FileInfo().Mode()
    @@ -388,13 +412,30 @@ func ExtractFile(dest string, hdr *tar.Header, cleanedName string, tr io.Reader)
     				return errors.Wrapf(err, "error removing %s to make way for new link", hdr.Name)
     			}
     		}
    -		link := filepath.Clean(filepath.Join(dest, hdr.Linkname))
    +		cleanedLink := filepath.Clean(hdr.Linkname)
    +		if cleanedLink == ".." || strings.HasPrefix(cleanedLink, "../") {
    +			return fmt.Errorf("hardlink target %q is not allowed: references parent directory", hdr.Linkname)
    +		}
    +		link, err := securejoin.SecureJoin(dest, hdr.Linkname)
    +		if err != nil {
    +			return fmt.Errorf("invalid hardlink target %q: %w", hdr.Linkname, err)
    +		}
     		if err := os.Link(link, path); err != nil {
     			return err
     		}
     
     	case tar.TypeSymlink:
     		logrus.Tracef("Symlink from %s to %s", hdr.Linkname, path)
    +		// Resolve the symlink target relative to the entry's parent directory
    +		// to get the effective path from the extraction root, then verify it
    +		// stays within the destination.
    +		effectivePath := filepath.Clean(filepath.Join(filepath.Dir(cleanedName), hdr.Linkname))
    +		if filepath.IsAbs(hdr.Linkname) {
    +			effectivePath = filepath.Clean(hdr.Linkname)
    +		}
    +		if effectivePath == ".." || strings.HasPrefix(effectivePath, "../") {
    +			return fmt.Errorf("symlink target %q resolves outside destination", hdr.Linkname)
    +		}
     		// The base directory for a symlink may not exist before it is created.
     		if err := os.MkdirAll(dir, 0o755); err != nil {
     			return err
    
  • pkg/util/fs_util_test.go+256 0 modified
    @@ -820,6 +820,262 @@ func TestExtractFile(t *testing.T) {
     	}
     }
     
    +func TestExtractFile_PathTraversal(t *testing.T) {
    +	defaultTestTime, err := time.Parse(time.RFC3339, "1912-06-23T00:00:00Z")
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	t.Run("regular file with dotdot", func(t *testing.T) {
    +		dest := t.TempDir()
    +		hdr := fileHeader("../outside.txt", "data", 0o644, defaultTestTime)
    +		err := ExtractFile(dest, hdr, filepath.Clean(hdr.Name), bytes.NewReader([]byte("data")))
    +		if err == nil {
    +			t.Fatal("expected error for parent directory reference, got nil")
    +		}
    +		if _, statErr := os.Stat(filepath.Join(dest, "..", "outside.txt")); statErr == nil {
    +			t.Fatal("file was written outside dest")
    +		}
    +	})
    +
    +	t.Run("directory with dotdot", func(t *testing.T) {
    +		dest := t.TempDir()
    +		hdr := dirHeader("../outsidedir", 0o755)
    +		err := ExtractFile(dest, hdr, filepath.Clean(hdr.Name), bytes.NewReader(nil))
    +		if err == nil {
    +			t.Fatal("expected error for parent directory reference, got nil")
    +		}
    +	})
    +
    +	t.Run("nested dotdot", func(t *testing.T) {
    +		dest := t.TempDir()
    +		hdr := fileHeader("foo/../../outside.txt", "data", 0o644, defaultTestTime)
    +		err := ExtractFile(dest, hdr, filepath.Clean(hdr.Name), bytes.NewReader([]byte("data")))
    +		if err == nil {
    +			t.Fatal("expected error for parent directory reference, got nil")
    +		}
    +	})
    +
    +	t.Run("hardlink target outside dest", func(t *testing.T) {
    +		dest := t.TempDir()
    +		legitimateHdr := fileHeader("./legit.txt", "hello", 0o644, defaultTestTime)
    +		if err := ExtractFile(dest, legitimateHdr, filepath.Clean(legitimateHdr.Name), bytes.NewReader([]byte("hello"))); err != nil {
    +			t.Fatal(err)
    +		}
    +		hdr := hardlinkHeader("./link", "../somefile")
    +		err := ExtractFile(dest, hdr, filepath.Clean(hdr.Name), bytes.NewReader(nil))
    +		if err == nil {
    +			t.Fatal("expected error for hardlink target outside dest, got nil")
    +		}
    +		if !strings.Contains(err.Error(), "references parent directory") {
    +			t.Fatalf("expected 'references parent directory' in error, got: %v", err)
    +		}
    +	})
    +
    +	t.Run("hardlink with absolute target is confined", func(t *testing.T) {
    +		dest := t.TempDir()
    +		// Create a file inside dest so there is something to link to after
    +		// securejoin confines the absolute path.
    +		legitimateHdr := fileHeader("etc/passwd", "confined", 0o644, defaultTestTime)
    +		if err := ExtractFile(dest, legitimateHdr, filepath.Clean(legitimateHdr.Name), bytes.NewReader([]byte("confined"))); err != nil {
    +			t.Fatal(err)
    +		}
    +		// Hardlink with absolute target — securejoin should confine it to dest.
    +		hdr := hardlinkHeader("./link", "/etc/passwd")
    +		err := ExtractFile(dest, hdr, filepath.Clean(hdr.Name), bytes.NewReader(nil))
    +		if err != nil {
    +			t.Fatalf("hardlink with absolute target should be confined, not rejected: %v", err)
    +		}
    +		// The link must resolve inside dest, not to the real /etc/passwd.
    +		got, err := os.ReadFile(filepath.Join(dest, "link"))
    +		if err != nil {
    +			t.Fatalf("reading link: %v", err)
    +		}
    +		if string(got) != "confined" {
    +			t.Fatalf("link content = %q, want %q (should point inside dest)", got, "confined")
    +		}
    +	})
    +
    +	t.Run("file through symlink is confined", func(t *testing.T) {
    +		dest := t.TempDir()
    +		outsideDir := t.TempDir()
    +
    +		// Create a symlink inside dest that points outside.
    +		symlinkHdr := linkHeader("./extlink", outsideDir)
    +		err := ExtractFile(dest, symlinkHdr, filepath.Clean(symlinkHdr.Name), bytes.NewReader(nil))
    +		if err != nil {
    +			t.Fatalf("symlink creation should succeed (absolute target is allowed): %v", err)
    +		}
    +
    +		// Verify the symlink was actually created on disk.
    +		target, err := os.Readlink(filepath.Join(dest, "extlink"))
    +		if err != nil {
    +			t.Fatalf("symlink was not created: %v", err)
    +		}
    +		if target != outsideDir {
    +			t.Fatalf("symlink target = %q, want %q", target, outsideDir)
    +		}
    +
    +		// Try to write a file through the symlink.
    +		writeHdr := fileHeader("./extlink/written.txt", "data", 0o644, defaultTestTime)
    +		_ = ExtractFile(dest, writeHdr, filepath.Clean(writeHdr.Name), bytes.NewReader([]byte("data")))
    +
    +		// The file must not appear outside dest.
    +		if _, statErr := os.Stat(filepath.Join(outsideDir, "written.txt")); statErr == nil {
    +			t.Fatal("file was written outside dest through symlink")
    +		}
    +	})
    +
    +	t.Run("symlink target outside dest via relative path", func(t *testing.T) {
    +		dest := t.TempDir()
    +		hdr := linkHeader("./link", "../outside")
    +		err := ExtractFile(dest, hdr, filepath.Clean(hdr.Name), bytes.NewReader(nil))
    +		if err == nil {
    +			t.Fatal("expected error for symlink target outside dest, got nil")
    +		}
    +		if !strings.Contains(err.Error(), "resolves outside destination") {
    +			t.Fatalf("unexpected error: %v", err)
    +		}
    +	})
    +
    +	t.Run("absolute symlink target resolves within dest", func(t *testing.T) {
    +		dest := t.TempDir()
    +		hdr := linkHeader("./link", "/subdir/target")
    +		err := ExtractFile(dest, hdr, filepath.Clean(hdr.Name), bytes.NewReader(nil))
    +		if err != nil {
    +			t.Fatalf("absolute symlink within dest should be allowed: %v", err)
    +		}
    +	})
    +
    +	t.Run("relative symlink within dest is allowed", func(t *testing.T) {
    +		dest := t.TempDir()
    +		os.MkdirAll(filepath.Join(dest, "foo"), 0o755)
    +		hdr := linkHeader("./foo/link", "../bar")
    +		err := ExtractFile(dest, hdr, filepath.Clean(hdr.Name), bytes.NewReader(nil))
    +		if err != nil {
    +			t.Fatalf("symlink within dest should be allowed: %v", err)
    +		}
    +	})
    +
    +	t.Run("legitimate file succeeds", func(t *testing.T) {
    +		dest := t.TempDir()
    +		hdr := fileHeader("./subdir/file.txt", "content", 0o644, defaultTestTime)
    +		err := ExtractFile(dest, hdr, filepath.Clean(hdr.Name), bytes.NewReader([]byte("content")))
    +		if err != nil {
    +			t.Fatalf("legitimate extraction should succeed: %v", err)
    +		}
    +		got, err := os.ReadFile(filepath.Join(dest, "subdir", "file.txt"))
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +		if string(got) != "content" {
    +			t.Fatalf("file contents = %q, want %q", got, "content")
    +		}
    +	})
    +}
    +
    +func TestUnTar_PathTraversal(t *testing.T) {
    +	makeTar := func(t *testing.T, hdrs ...tar.Header) *bytes.Buffer {
    +		t.Helper()
    +		var buf bytes.Buffer
    +		tw := tar.NewWriter(&buf)
    +		for _, hdr := range hdrs {
    +			h := hdr
    +			if err := tw.WriteHeader(&h); err != nil {
    +				t.Fatal(err)
    +			}
    +			if h.Size > 0 {
    +				if _, err := tw.Write(make([]byte, h.Size)); err != nil {
    +					t.Fatal(err)
    +				}
    +			}
    +		}
    +		if err := tw.Close(); err != nil {
    +			t.Fatal(err)
    +		}
    +		return &buf
    +	}
    +
    +	t.Run("entry with dotdot is rejected", func(t *testing.T) {
    +		buf := makeTar(t, tar.Header{
    +			Name: "../outside.txt", Size: 5, Mode: 0o644, Typeflag: tar.TypeReg,
    +		})
    +		dest := t.TempDir()
    +		if _, err := UnTar(buf, dest); err == nil {
    +			t.Fatal("expected error for parent directory reference, got nil")
    +		}
    +		// Verify no file was written outside dest.
    +		if _, err := os.Stat(filepath.Join(dest, "..", "outside.txt")); err == nil {
    +			t.Fatal("file was written outside dest")
    +		}
    +	})
    +
    +	t.Run("symlink with dotdot target is rejected", func(t *testing.T) {
    +		buf := makeTar(t, tar.Header{
    +			Name: "ext-link", Typeflag: tar.TypeSymlink, Linkname: "../../etc/passwd",
    +		})
    +		dest := t.TempDir()
    +		if _, err := UnTar(buf, dest); err == nil {
    +			t.Fatal("expected error for symlink with dotdot target, got nil")
    +		}
    +	})
    +
    +	t.Run("hardlink with dotdot target is rejected", func(t *testing.T) {
    +		buf := makeTar(t,
    +			tar.Header{
    +				Name: "legit.txt", Size: 5, Mode: 0o644, Typeflag: tar.TypeReg,
    +				Uid: os.Getuid(), Gid: os.Getgid(),
    +			},
    +			tar.Header{
    +				Name: "link", Typeflag: tar.TypeLink, Linkname: "../etc/passwd",
    +			},
    +		)
    +		dest := t.TempDir()
    +		if _, err := UnTar(buf, dest); err == nil {
    +			t.Fatal("expected error for hardlink with dotdot target, got nil")
    +		}
    +	})
    +
    +	t.Run("file through symlink is confined", func(t *testing.T) {
    +		outsideDir := t.TempDir()
    +		dest := t.TempDir()
    +
    +		// Tar contains a symlink pointing to an absolute path outside dest,
    +		// followed by a file written under that symlink name.
    +		buf := makeTar(t,
    +			tar.Header{
    +				Name: "extlink", Typeflag: tar.TypeSymlink, Linkname: outsideDir,
    +			},
    +			tar.Header{
    +				Name: "extlink/written.txt", Size: 5, Mode: 0o644, Typeflag: tar.TypeReg,
    +				Uid: os.Getuid(), Gid: os.Getgid(),
    +			},
    +		)
    +		// Extraction may or may not return an error; either way the file
    +		// must not appear outside dest.
    +		UnTar(buf, dest)
    +
    +		if _, err := os.Stat(filepath.Join(outsideDir, "written.txt")); err == nil {
    +			t.Fatal("file was written outside dest through symlink")
    +		}
    +	})
    +
    +	t.Run("legitimate entries succeed", func(t *testing.T) {
    +		buf := makeTar(t, tar.Header{
    +			Name: "subdir/file.txt", Size: 5, Mode: 0o644, Typeflag: tar.TypeReg,
    +			Uid: os.Getuid(), Gid: os.Getgid(),
    +		})
    +		dest := t.TempDir()
    +		files, err := UnTar(buf, dest)
    +		if err != nil {
    +			t.Fatalf("legitimate extraction should succeed: %v", err)
    +		}
    +		if len(files) != 1 {
    +			t.Fatalf("expected 1 file, got %d", len(files))
    +		}
    +	})
    +}
    +
     func TestCopySymlink(t *testing.T) {
     	type tc struct {
     		name       string
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.