VYPR
Medium severity6.5NVD Advisory· Published Apr 6, 2026· Updated Apr 28, 2026

CVE-2026-35454

CVE-2026-35454

Description

The Code Extension Marketplace is an open-source alternative to the VS Code Marketplace. Prior to 2.4.2, Zip Slip vulnerability in coder/code-marketplace allowed a malicious VSIX file to write arbitrary files outside the extension directory. ExtractZip passed raw zip entry names to a callback that wrote files via filepath.Join with no boundary check; filepath.Join resolved .. components but did not prevent the result from escaping the base path. This vulnerability is fixed in 2.4.2.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/coder/code-marketplaceGo
< 1.2.3-0.20260402184705-988440dee05f1.2.3-0.20260402184705-988440dee05f

Affected products

1

Patches

1
988440dee05f

fix: improve archive extraction

https://github.com/coder/code-marketplaceJakub DomerackiMar 26, 2026via ghsa
5 files changed · +155 19
  • go.mod+1 1 modified
    @@ -1,6 +1,6 @@
     module github.com/coder/code-marketplace
     
    -go 1.24.11
    +go 1.25.8
     
     require (
     	cdr.dev/slog v1.6.1
    
  • storage/local.go+32 12 modified
    @@ -94,20 +94,32 @@ func (s *Local) list(ctx context.Context) []extension {
     	return list
     }
     
    +
     func (s *Local) AddExtension(ctx context.Context, manifest *VSIXManifest, vsix []byte, extra ...File) (string, error) {
     	// Extract the zip to the correct path.
     	identity := manifest.Metadata.Identity
     	dir := filepath.Join(s.extdir, identity.Publisher, identity.ID, Version{
     		Version:        identity.Version,
     		TargetPlatform: identity.TargetPlatform,
     	}.String())
    -	err := easyzip.ExtractZip(vsix, func(name string, r io.Reader) error {
    -		path := filepath.Join(dir, name)
    -		err := os.MkdirAll(filepath.Dir(path), 0o755)
    -		if err != nil {
    +
    +	// Ensure the target directory exists before opening a root on it.
    +	if err := os.MkdirAll(dir, 0o755); err != nil {
    +		return "", err
    +	}
    +	// os.Root restricts all file operations to dir, preventing path traversal
    +	// via ".." components, absolute paths, and symlink escapes (Go 1.24+).
    +	root, err := os.OpenRoot(dir)
    +	if err != nil {
    +		return "", err
    +	}
    +	defer root.Close()
    +
    +	err = easyzip.ExtractZip(vsix, func(name string, r io.Reader) error {
    +		if err := root.MkdirAll(filepath.Dir(name), 0o755); err != nil {
     			return err
     		}
    -		w, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
    +		w, err := root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
     		if err != nil {
     			return err
     		}
    @@ -120,21 +132,29 @@ func (s *Local) AddExtension(ctx context.Context, manifest *VSIXManifest, vsix [
     	}
     
     	// Copy the VSIX itself as well.
    -	vsixPath := filepath.Join(dir, fmt.Sprintf("%s.vsix", ExtensionVSIXNameFromManifest(manifest)))
    -	err = os.WriteFile(vsixPath, vsix, 0o644)
    +	vsixName := fmt.Sprintf("%s.vsix", ExtensionVSIXNameFromManifest(manifest))
    +	w, err := root.OpenFile(vsixName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
    +	if err != nil {
    +		return "", err
    +	}
    +	_, err = w.Write(vsix)
    +	w.Close()
     	if err != nil {
     		return "", err
     	}
     
     	for _, file := range extra {
    -		path := filepath.Join(dir, file.RelativePath)
    -		err := os.MkdirAll(filepath.Dir(path), 0o755)
    -		if err != nil {
    +		if err := root.MkdirAll(filepath.Dir(file.RelativePath), 0o755); err != nil {
     			return "", err
     		}
    -		err = os.WriteFile(path, file.Content, 0o644)
    +		w, err := root.OpenFile(file.RelativePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
    +		if err != nil {
    +			return dir, xerrors.Errorf("write extra file %q: %w", file.RelativePath, err)
    +		}
    +		_, err = w.Write(file.Content)
    +		w.Close()
     		if err != nil {
    -			return dir, xerrors.Errorf("write extra file %q: %w", path, err)
    +			return dir, xerrors.Errorf("write extra file %q: %w", file.RelativePath, err)
     		}
     	}
     
    
  • storage/local_test.go+1 0 modified
    @@ -19,6 +19,7 @@ func localFactory(t *testing.T) testStorage {
     	require.NoError(t, err)
     	return testStorage{
     		storage: s,
    +		dir:     extdir,
     		write: func(content []byte, elem ...string) {
     			dest := filepath.Join(extdir, filepath.Join(elem...))
     			err := os.MkdirAll(filepath.Dir(dest), 0o755)
    
  • storage/signature_test.go+1 0 modified
    @@ -30,6 +30,7 @@ func signed(signer bool, factory func(t *testing.T) testStorage) func(t *testing
     			storage:          storage.NewSignatureStorage(slog.Make(), key, st.storage),
     			write:            st.write,
     			exists:           st.exists,
    +			dir:              st.dir,
     			expectedManifest: exp,
     		}
     	}
    
  • storage/storage_test.go+120 6 modified
    @@ -1,6 +1,8 @@
     package storage_test
     
     import (
    +	"archive/zip"
    +	"bytes"
     	"context"
     	"errors"
     	"fmt"
    @@ -25,6 +27,8 @@ type testStorage struct {
     	storage storage.Storage
     	write   func(content []byte, elem ...string)
     	exists  func(elem ...string) bool
    +	// dir is the base extension directory (local storage only).
    +	dir string
     
     	expectedManifest func(man *storage.VSIXManifest)
     }
    @@ -123,20 +127,23 @@ func TestNewStorage(t *testing.T) {
     func TestStorage(t *testing.T) {
     	t.Parallel()
     	factories := []struct {
    -		name    string
    -		factory storageFactory
    +		name      string
    +		factory   storageFactory
    +		localOnly bool
     	}{
     		{
    -			name:    "Local",
    -			factory: localFactory,
    +			name:      "Local",
    +			factory:   localFactory,
    +			localOnly: true,
     		},
     		{
     			name:    "Artifactory",
     			factory: artifactoryFactory,
     		},
     		{
    -			name:    "SignedLocal",
    -			factory: signed(true, localFactory),
    +			name:      "SignedLocal",
    +			factory:   signed(true, localFactory),
    +			localOnly: true,
     		},
     		{
     			name:    "SignedArtifactory",
    @@ -148,6 +155,23 @@ func TestStorage(t *testing.T) {
     			t.Run("AddExtension", func(t *testing.T) {
     				testAddExtension(t, sf.factory)
     			})
    +			if sf.localOnly {
    +				t.Run("AddExtensionZipTraversal", func(t *testing.T) {
    +					testAddExtensionZipTraversal(t, sf.factory)
    +				})
    +				t.Run("AddExtensionExtraTraversal", func(t *testing.T) {
    +					testAddExtensionExtraTraversal(t, sf.factory)
    +				})
    +				t.Run("AddExtensionZipAbsolutePath", func(t *testing.T) {
    +					testAddExtensionZipAbsolutePath(t, sf.factory)
    +				})
    +				t.Run("AddExtensionExtraAbsolutePath", func(t *testing.T) {
    +					testAddExtensionExtraAbsolutePath(t, sf.factory)
    +				})
    +				t.Run("AddExtensionSymlinkEscape", func(t *testing.T) {
    +					testAddExtensionSymlinkEscape(t, sf.factory)
    +				})
    +			}
     			t.Run("RemoveExtension", func(t *testing.T) {
     				testRemoveExtension(t, sf.factory)
     			})
    @@ -927,6 +951,96 @@ func testAddExtension(t *testing.T, factory storageFactory) {
     	}
     }
     
    +// createTraversalVSIX returns a valid zip whose sole entry has a path-traversal
    +// name, used to test zip-slip protection in AddExtension.
    +func createTraversalVSIX(t *testing.T, entryName string) []byte {
    +	t.Helper()
    +	buf := bytes.NewBuffer(nil)
    +	zw := zip.NewWriter(buf)
    +	fw, err := zw.Create(entryName)
    +	require.NoError(t, err)
    +	_, err = fw.Write([]byte("evil"))
    +	require.NoError(t, err)
    +	require.NoError(t, zw.Close())
    +	return buf.Bytes()
    +}
    +
    +func testAddExtensionZipTraversal(t *testing.T, factory storageFactory) {
    +	t.Parallel()
    +
    +	f := factory(t)
    +	ext := testutil.Extensions[0]
    +	manifest := testutil.ConvertExtensionToManifest(ext, storage.Version{Version: ext.LatestVersion})
    +	vsix := createTraversalVSIX(t, "../../../../tmp/evil")
    +	_, err := f.storage.AddExtension(context.Background(), manifest, vsix)
    +	require.Error(t, err)
    +	require.Contains(t, err.Error(), "path escapes from parent")
    +}
    +
    +func testAddExtensionExtraTraversal(t *testing.T, factory storageFactory) {
    +	t.Parallel()
    +
    +	f := factory(t)
    +	ext := testutil.Extensions[0]
    +	manifest := testutil.ConvertExtensionToManifest(ext, storage.Version{Version: ext.LatestVersion})
    +	vsix := testutil.CreateVSIXFromManifest(t, manifest)
    +	evil := storage.File{RelativePath: "../../../../tmp/evil", Content: []byte("evil")}
    +	_, err := f.storage.AddExtension(context.Background(), manifest, vsix, evil)
    +	require.Error(t, err)
    +	require.Contains(t, err.Error(), "path escapes from parent")
    +}
    +
    +func testAddExtensionZipAbsolutePath(t *testing.T, factory storageFactory) {
    +	t.Parallel()
    +
    +	f := factory(t)
    +	ext := testutil.Extensions[0]
    +	manifest := testutil.ConvertExtensionToManifest(ext, storage.Version{Version: ext.LatestVersion})
    +	vsix := createTraversalVSIX(t, "/tmp/evil")
    +	_, err := f.storage.AddExtension(context.Background(), manifest, vsix)
    +	require.Error(t, err)
    +	require.Contains(t, err.Error(), "path escapes from parent")
    +}
    +
    +func testAddExtensionExtraAbsolutePath(t *testing.T, factory storageFactory) {
    +	t.Parallel()
    +
    +	f := factory(t)
    +	ext := testutil.Extensions[0]
    +	manifest := testutil.ConvertExtensionToManifest(ext, storage.Version{Version: ext.LatestVersion})
    +	vsix := testutil.CreateVSIXFromManifest(t, manifest)
    +	evil := storage.File{RelativePath: "/tmp/evil", Content: []byte("evil")}
    +	_, err := f.storage.AddExtension(context.Background(), manifest, vsix, evil)
    +	require.Error(t, err)
    +	require.Contains(t, err.Error(), "path escapes from parent")
    +}
    +
    +func testAddExtensionSymlinkEscape(t *testing.T, factory storageFactory) {
    +	t.Parallel()
    +
    +	f := factory(t)
    +	ext := testutil.Extensions[0]
    +	manifest := testutil.ConvertExtensionToManifest(ext, storage.Version{Version: ext.LatestVersion})
    +	vsix := testutil.CreateVSIXFromManifest(t, manifest)
    +
    +	// Pre-create the extension directory and plant a symlink inside it that
    +	// points to a directory outside the root.  An extra file written through
    +	// this symlink must be rejected by os.Root's symlink-escape protection.
    +	identity := manifest.Metadata.Identity
    +	extDir := filepath.Join(f.dir, identity.Publisher, identity.ID, identity.Version)
    +	require.NoError(t, os.MkdirAll(extDir, 0o755))
    +	outside := t.TempDir()
    +	require.NoError(t, os.Symlink(outside, filepath.Join(extDir, "link")))
    +
    +	evil := storage.File{RelativePath: "link/evil", Content: []byte("evil")}
    +	_, err := f.storage.AddExtension(context.Background(), manifest, vsix, evil)
    +	require.Error(t, err)
    +
    +	// Confirm the file was not written to the target outside the root.
    +	_, statErr := os.Stat(filepath.Join(outside, "evil"))
    +	require.True(t, os.IsNotExist(statErr))
    +}
    +
     func testRemoveExtension(t *testing.T, factory storageFactory) {
     	t.Parallel()
     
    

Vulnerability mechanics

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

References

5

News mentions

0

No linked articles in our index yet.