From beb70891e5599f1175b90027fdaaf8ffdfb9a584 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 16 Dec 2025 08:28:12 -0500 Subject: [PATCH] unapply base path for resolver inbound requests (#4478) Signed-off-by: Alex Goodman --- syft/internal/fileresolver/chroot_context.go | 35 ++++++-- .../fileresolver/chroot_context_test.go | 82 +++++++++++++++++++ 2 files changed, 108 insertions(+), 9 deletions(-) diff --git a/syft/internal/fileresolver/chroot_context.go b/syft/internal/fileresolver/chroot_context.go index 3a718635d..88647be3a 100644 --- a/syft/internal/fileresolver/chroot_context.go +++ b/syft/internal/fileresolver/chroot_context.go @@ -14,10 +14,11 @@ import ( // the user given root, the base path (if any) to consider as the root, and the current working directory. // Note: this only works on a real filesystem, not on a virtual filesystem (such as a stereoscope filetree). type ChrootContext struct { - root string - base string - cwd string - cwdRelativeToRoot string + root string + rootRelativeToBase string + base string + cwd string + cwdRelativeToRoot string } func NewChrootContextFromCWD(root, base string) (*ChrootContext, error) { @@ -40,10 +41,26 @@ func NewChrootContext(root, base, cwd string) (*ChrootContext, error) { return nil, err } + // we need to track the relative path from root to base (if set) so that request paths can un-apply the base path + // changes from any incoming requests. + var rootRelativeToBase string + if cleanBase != cleanRoot && cleanBase != "" { + absRoot := cleanRoot + if !filepath.IsAbs(cleanRoot) { + absRoot = filepath.Join(cwd, cleanRoot) + } + + rootRelativeToBase, err = filepath.Rel(absRoot, cleanBase) // validate that base is within root + if err != nil { + return nil, fmt.Errorf("base path %q is not within root path %q: %w", cleanBase, cleanRoot, err) + } + } + chroot := &ChrootContext{ - root: cleanRoot, - base: cleanBase, - cwd: cwd, + root: cleanRoot, + rootRelativeToBase: rootRelativeToBase, + base: cleanBase, + cwd: cwd, } return chroot, chroot.ChangeDirectory(cwd) @@ -125,8 +142,8 @@ func (r ChrootContext) ToNativePath(chrootPath string) (string, error) { responsePath := chrootPath if filepath.IsAbs(responsePath) { - // don't allow input to potentially hop above root path - responsePath = path.Join(r.root, responsePath) + // don't allow input to potentially hop above root path (and still un-apply any base paths) + responsePath = path.Join(r.root, r.rootRelativeToBase, responsePath) } else { // ensure we take into account any relative difference between the root path and the CWD for relative requests responsePath = path.Join(r.cwdRelativeToRoot, responsePath) diff --git a/syft/internal/fileresolver/chroot_context_test.go b/syft/internal/fileresolver/chroot_context_test.go index 71621efa5..96df93aea 100644 --- a/syft/internal/fileresolver/chroot_context_test.go +++ b/syft/internal/fileresolver/chroot_context_test.go @@ -452,6 +452,25 @@ func Test_ChrootContext_RequestResponse(t *testing.T) { expectedNativePath: absRelOutsidePath, expectedChrootPath: "to/the/rel-outside.txt", }, + // base path within root cases... + // note: for absolute input paths, rootRelativeToBase is used to resolve the native path + // note: for relative input paths, cwdRelativeToRoot is used (base does not affect relative path resolution) + { + name: "relative root, abs request, with base", + root: relative, + base: filepath.Join(relative, "path", "to"), + input: "/the/file.txt", + expectedNativePath: absPathToTheFile, + expectedChrootPath: "/the/file.txt", // ToChrootPath trims base prefix without adding separator + }, + { + name: "abs root, abs request, with base", + root: absolute, + base: filepath.Join(absolute, "path", "to"), + input: "/the/file.txt", + expectedNativePath: absPathToTheFile, + expectedChrootPath: "/the/file.txt", // ToChrootPath trims base prefix without adding separator + }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -480,6 +499,69 @@ func Test_ChrootContext_RequestResponse(t *testing.T) { } } +func TestNewChrootContext_BaseValidation(t *testing.T) { + testDir, err := os.Getwd() + require.NoError(t, err) + + relative := filepath.Join("test-fixtures", "req-resp") + absolute := filepath.Join(testDir, relative) + + tests := []struct { + name string + root string + base string + cwd string + expectedRootRelativeToBase string + wantErr require.ErrorAssertionFunc + }{ + { + name: "base within root", + root: absolute, + base: filepath.Join(absolute, "path", "to"), + cwd: testDir, + expectedRootRelativeToBase: filepath.Join("path", "to"), + }, + { + name: "base equals root", + root: absolute, + base: absolute, + cwd: testDir, + expectedRootRelativeToBase: "", + }, + { + name: "empty base", + root: absolute, + base: "", + cwd: testDir, + expectedRootRelativeToBase: "", + }, + { + name: "relative root with base", + root: relative, + base: filepath.Join(absolute, "path", "to"), + cwd: testDir, + expectedRootRelativeToBase: filepath.Join("path", "to"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantErr == nil { + tt.wantErr = require.NoError + } + + ctx, err := NewChrootContext(tt.root, tt.base, tt.cwd) + tt.wantErr(t, err) + + if err != nil { + return + } + + assert.Equal(t, tt.expectedRootRelativeToBase, ctx.rootRelativeToBase) + }) + } +} + func TestToNativeGlob(t *testing.T) { tests := []struct { name string