unapply base path for resolver inbound requests (#4478)

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2025-12-16 08:28:12 -05:00 committed by GitHub
parent e0b61a3ae3
commit beb70891e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 108 additions and 9 deletions

View File

@ -14,10 +14,11 @@ import (
// the user given root, the base path (if any) to consider as the root, and the current working directory. // 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). // Note: this only works on a real filesystem, not on a virtual filesystem (such as a stereoscope filetree).
type ChrootContext struct { type ChrootContext struct {
root string root string
base string rootRelativeToBase string
cwd string base string
cwdRelativeToRoot string cwd string
cwdRelativeToRoot string
} }
func NewChrootContextFromCWD(root, base string) (*ChrootContext, error) { func NewChrootContextFromCWD(root, base string) (*ChrootContext, error) {
@ -40,10 +41,26 @@ func NewChrootContext(root, base, cwd string) (*ChrootContext, error) {
return nil, err 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{ chroot := &ChrootContext{
root: cleanRoot, root: cleanRoot,
base: cleanBase, rootRelativeToBase: rootRelativeToBase,
cwd: cwd, base: cleanBase,
cwd: cwd,
} }
return chroot, chroot.ChangeDirectory(cwd) return chroot, chroot.ChangeDirectory(cwd)
@ -125,8 +142,8 @@ func (r ChrootContext) ToNativePath(chrootPath string) (string, error) {
responsePath := chrootPath responsePath := chrootPath
if filepath.IsAbs(responsePath) { if filepath.IsAbs(responsePath) {
// don't allow input to potentially hop above root path // don't allow input to potentially hop above root path (and still un-apply any base paths)
responsePath = path.Join(r.root, responsePath) responsePath = path.Join(r.root, r.rootRelativeToBase, responsePath)
} else { } else {
// ensure we take into account any relative difference between the root path and the CWD for relative requests // ensure we take into account any relative difference between the root path and the CWD for relative requests
responsePath = path.Join(r.cwdRelativeToRoot, responsePath) responsePath = path.Join(r.cwdRelativeToRoot, responsePath)

View File

@ -452,6 +452,25 @@ func Test_ChrootContext_RequestResponse(t *testing.T) {
expectedNativePath: absRelOutsidePath, expectedNativePath: absRelOutsidePath,
expectedChrootPath: "to/the/rel-outside.txt", 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 { for _, c := range cases {
t.Run(c.name, func(t *testing.T) { 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) { func TestToNativeGlob(t *testing.T) {
tests := []struct { tests := []struct {
name string name string