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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/coder/code-marketplaceGo | < 1.2.3-0.20260402184705-988440dee05f | 1.2.3-0.20260402184705-988440dee05f |
Affected products
1Patches
1988440dee05ffix: improve archive extraction
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- github.com/advisories/GHSA-8x9r-hvwg-c55hghsaADVISORY
- github.com/coder/code-marketplace/security/advisories/GHSA-8x9r-hvwg-c55hnvdThird Party AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-35454ghsaADVISORY
- github.com/coder/code-marketplace/commit/988440dee05fceef8400ed725badc604dbf90792ghsaWEB
- github.com/coder/code-marketplace/releases/tag/v2.4.2nvdRelease NotesWEB
News mentions
0No linked articles in our index yet.