VYPR
High severity7.7NVD Advisory· Published Jun 10, 2026· Updated Jun 10, 2026

CVE-2026-50567

CVE-2026-50567

Description

Fission versions prior to 1.25.0 are vulnerable to Zip Slip, allowing attackers to write files anywhere on the system.

AI Insight

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

Fission versions prior to 1.25.0 are vulnerable to Zip Slip, allowing attackers to write files anywhere on the system.

Vulnerability

Fission versions prior to 1.25.0 contain a Zip Slip vulnerability in the Unarchive function within pkg/utils/zip.go. This function joins archive entry names with the destination directory using filepath.Join without validating that the resolved path remains within the destination. This allows a crafted zip entry, such as ../../tmp/evil, to be written to /tmp/evil [2].

Exploitation

An attacker can exploit this vulnerability by controlling a Package.Spec.Source.URL or Deployment.URL that points to a malicious zip archive. The fission-fetcher sidecar, running as a per-environment pod, will download and extract this archive. The attacker needs to be an authenticated Kubernetes user with Package CRD create permission. The KeepArchive setting must be false, which is the default [2].

Impact

Successful exploitation allows an attacker to write files anywhere the fission-fetcher process has write access. This could include other tenants' /packages// directories, mounted secret or config volumes, or even the fetcher's own binary. This could lead to arbitrary file writes and potentially compromise the Kubernetes cluster [2].

Mitigation

This vulnerability has been fixed in Fission version 1.25.0, released on 2024-01-17 [1]. The fix involves confining zip extraction using os.Root, validating archive entry names to reject absolute paths and .. traversal, and refusing symlink entries upfront [3].

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

Affected products

2
  • Fission/Fissionreferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • (no CPE)range: <1.25.0

Patches

1
55704aca1b8d

fix(utils): confine zip extraction to the destination with os.Root (zip-slip) (#3444)

https://github.com/fission/fissionSanket SudakeJun 1, 2026via body-scan-shorthand
2 files changed · +142 11
  • pkg/utils/zip.go+44 11 modified
    @@ -10,6 +10,7 @@ import (
     	"io"
     	"os"
     	"path/filepath"
    +	"strings"
     
     	"github.com/mholt/archives"
     )
    @@ -74,7 +75,13 @@ func Archive(ctx context.Context, src string, dst string) error {
     	return err
     }
     
    -// Unarchive is a function that unzips a zip file to destination
    +// Unarchive unzips the zip file at src into dst.
    +//
    +// Extraction is confined to dst through an os.Root: the archive entry name
    +// arrives from a user-supplied package, so a crafted name (e.g. "../../etc/x"
    +// or an absolute path) must not write outside dst. os.Root enforces that in the
    +// kernel; we also reject escaping names and symlink entries up front for a
    +// clear error (zip-slip / CWE-22).
     func Unarchive(ctx context.Context, src string, dst string) error {
     	var format archives.Zip
     	file, err := os.Open(src)
    @@ -83,20 +90,46 @@ func Unarchive(ctx context.Context, src string, dst string) error {
     	}
     	defer file.Close()
     
    +	// The destination must exist before we can open a root on it. Mode 0o755 is
    +	// required so that other containers in the same pod (running under
    +	// different UIDs / GIDs — fetcher sidecar at UID 10001 vs. builder
    +	// running as root) can read this shared /packages volume. Tighter
    +	// modes break cross-container access for the v2 builder flow.
    +	if err := os.MkdirAll(dst, 0o755); err != nil {
    +		return fmt.Errorf("failed to create destination directory: %w", err)
    +	}
    +	root, err := os.OpenRoot(dst)
    +	if err != nil {
    +		return fmt.Errorf("failed to open destination root: %w", err)
    +	}
    +	defer root.Close()
    +
     	return format.Extract(ctx, file, func(ctx context.Context, f archives.FileInfo) error {
    -		destPath := filepath.Join(dst, f.NameInArchive)
    +		// Confine the archive entry to dst.
    +		name := filepath.Clean(filepath.FromSlash(f.NameInArchive))
    +		if name == "." {
    +			return nil // archive root; dst already exists
    +		}
    +		if filepath.IsAbs(name) || name == ".." || strings.HasPrefix(name, ".."+string(os.PathSeparator)) {
    +			return fmt.Errorf("archive entry %q escapes destination", f.NameInArchive)
    +		}
    +		// Refuse symlink entries: function packages have no need for them, and
    +		// they are an avenue for escaping the extraction root.
    +		if f.Mode()&os.ModeSymlink != 0 {
    +			return fmt.Errorf("archive entry %q is a symlink; refusing to extract", f.NameInArchive)
    +		}
    +
     		// check if the file is a directory
     		if f.IsDir() {
    -			return os.MkdirAll(destPath, f.Mode())
    +			return root.MkdirAll(name, f.Mode().Perm())
     		}
     
    -		// check if parent directory exists for the file. Mode 0o755 is
    -		// required so that other containers in the same pod (running under
    -		// different UIDs / GIDs — fetcher sidecar at UID 10001 vs. builder
    -		// running as root) can read this shared /packages volume. Tighter
    -		// modes break cross-container access for the v2 builder flow.
    -		if err := os.MkdirAll(filepath.Dir(destPath), os.ModeDir|0o755); err != nil {
    -			return fmt.Errorf("failed to create parent directory: %w", err)
    +		// Create the parent directory at 0o755 for the same cross-container
    +		// access reason as the destination root above.
    +		if dir := filepath.Dir(name); dir != "." {
    +			if err := root.MkdirAll(dir, 0o755); err != nil {
    +				return fmt.Errorf("failed to create parent directory: %w", err)
    +			}
     		}
     
     		// Open file in archive
    @@ -109,7 +142,7 @@ func Unarchive(ctx context.Context, src string, dst string) error {
     		// Create file in destination with the archive entry's mode applied
     		// at create time, so a concurrent observer never sees a wider mode.
     		// Same overwrite semantics as os.Create.
    -		destFile, err := os.OpenFile(destPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, f.Mode().Perm())
    +		destFile, err := root.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, f.Mode().Perm())
     		if err != nil {
     			return fmt.Errorf("failed to create file in destination: %w", err)
     		}
    
  • pkg/utils/zip_test.go+98 0 modified
    @@ -5,13 +5,111 @@
     package utils
     
     import (
    +	"archive/zip"
     	"os"
     	"path/filepath"
     	"testing"
     
    +	"github.com/stretchr/testify/assert"
     	"github.com/stretchr/testify/require"
     )
     
    +type rawZipEntry struct {
    +	name string
    +	mode os.FileMode
    +	body string
    +}
    +
    +// writeRawZip writes a zip whose entry names and modes are set verbatim,
    +// bypassing any archive-time sanitization so that malicious names (the kind an
    +// attacker-supplied package can contain) reach the extractor unchanged.
    +func writeRawZip(t *testing.T, path string, entries ...rawZipEntry) {
    +	t.Helper()
    +	f, err := os.Create(path)
    +	require.NoError(t, err)
    +	defer f.Close()
    +	zw := zip.NewWriter(f)
    +	for _, e := range entries {
    +		hdr := &zip.FileHeader{Name: e.name, Method: zip.Deflate}
    +		hdr.SetMode(e.mode)
    +		w, err := zw.CreateHeader(hdr)
    +		require.NoError(t, err)
    +		_, err = w.Write([]byte(e.body))
    +		require.NoError(t, err)
    +	}
    +	require.NoError(t, zw.Close())
    +}
    +
    +// TestUnarchiveZipSlip verifies that a malicious archive entry cannot write
    +// outside the destination directory (zip-slip / CWE-22) and that symlink
    +// entries are refused, while benign archives still extract.
    +func TestUnarchiveZipSlip(t *testing.T) {
    +	t.Parallel()
    +	ctx := t.Context()
    +
    +	t.Run("parent traversal is refused", func(t *testing.T) {
    +		t.Parallel()
    +		tmp := t.TempDir()
    +		sentinel := filepath.Join(tmp, "sentinel")
    +		require.NoError(t, os.WriteFile(sentinel, []byte("intact"), 0o600))
    +
    +		zipPath := filepath.Join(tmp, "evil.zip")
    +		writeRawZip(t, zipPath, rawZipEntry{name: "../escape.txt", mode: 0o644, body: "pwned"})
    +
    +		err := Unarchive(ctx, zipPath, filepath.Join(tmp, "dst"))
    +		assert.Error(t, err)
    +		assert.NoFileExists(t, filepath.Join(tmp, "escape.txt"))
    +
    +		got, err := os.ReadFile(sentinel)
    +		require.NoError(t, err)
    +		assert.Equal(t, "intact", string(got))
    +	})
    +
    +	t.Run("absolute path is refused", func(t *testing.T) {
    +		t.Parallel()
    +		tmp := t.TempDir()
    +		abs := filepath.Join(tmp, "abs-escape.txt")
    +		zipPath := filepath.Join(tmp, "evil.zip")
    +		writeRawZip(t, zipPath, rawZipEntry{name: abs, mode: 0o644, body: "pwned"})
    +
    +		err := Unarchive(ctx, zipPath, filepath.Join(tmp, "dst"))
    +		assert.Error(t, err)
    +		assert.NoFileExists(t, abs)
    +	})
    +
    +	t.Run("symlink entry is refused", func(t *testing.T) {
    +		t.Parallel()
    +		tmp := t.TempDir()
    +		zipPath := filepath.Join(tmp, "evil.zip")
    +		writeRawZip(t, zipPath, rawZipEntry{name: "link", mode: 0o777 | os.ModeSymlink, body: "/etc/passwd"})
    +
    +		dst := filepath.Join(tmp, "dst")
    +		err := Unarchive(ctx, zipPath, dst)
    +		assert.Error(t, err)
    +		_, lerr := os.Lstat(filepath.Join(dst, "link"))
    +		assert.True(t, os.IsNotExist(lerr), "no symlink should be created")
    +	})
    +
    +	t.Run("benign archive still extracts", func(t *testing.T) {
    +		t.Parallel()
    +		tmp := t.TempDir()
    +		zipPath := filepath.Join(tmp, "ok.zip")
    +		writeRawZip(t, zipPath,
    +			rawZipEntry{name: "a.txt", mode: 0o644, body: "alpha"},
    +			rawZipEntry{name: "sub/b.txt", mode: 0o644, body: "beta"},
    +		)
    +		dst := filepath.Join(tmp, "dst")
    +		require.NoError(t, Unarchive(ctx, zipPath, dst))
    +
    +		got, err := os.ReadFile(filepath.Join(dst, "a.txt"))
    +		require.NoError(t, err)
    +		assert.Equal(t, "alpha", string(got))
    +		got, err = os.ReadFile(filepath.Join(dst, "sub", "b.txt"))
    +		require.NoError(t, err)
    +		assert.Equal(t, "beta", string(got))
    +	})
    +}
    +
     func TestIsZip(t *testing.T) {
     	tests := []struct {
     		name    string
    

Vulnerability mechanics

Root cause

"The Unarchive function in pkg/utils/zip.go did not validate archive entry paths, allowing directory traversal."

Attack vector

An attacker with Package CRD create permission can create a Package with a Source.URL or Deployment.URL pointing to a malicious zip archive [ref_id=1]. When the fission-fetcher sidecar downloads and extracts this archive, it will write files outside the intended destination directory due to a lack of path validation [ref_id=1]. This can lead to overwriting sensitive files, other tenants' data, or even the fetcher's binary.

Affected code

The vulnerability resides in the Unarchive function located in pkg/utils/zip.go. Specifically, the code joins archive entry names with the destination directory using filepath.Join without verifying if the resolved path remains within the destination [ref_id=1].

What the fix does

The patch introduces path validation within the Unarchive function. It now opens an os.Root on the destination, rejects absolute paths and parent directory traversal entries (e.g., '../../'), and refuses symlink entries upfront [patch_id=5504362]. By confining all file operations to the destination directory at the kernel level, the fix prevents archive entries from being written outside the intended scope.

Preconditions

  • authAuthenticated Kubernetes user with Package CRD create permission.
  • inputUser creates a Package with Source.URL or Deployment.URL pointing to an attacker-hosted zip archive.
  • configKeepArchive set to false (default).

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

References

3

News mentions

1