diff --git a/weed/server/filer_jwt_test.go b/weed/server/filer_jwt_test.go index bc9709c27..b66c085dd 100644 --- a/weed/server/filer_jwt_test.go +++ b/weed/server/filer_jwt_test.go @@ -137,6 +137,54 @@ func TestFilerServer_maybeCheckJwtAuthorization_Scoped(t *testing.T) { isWrite: false, expectAuthorized: true, }, + { + name: "tenant prefix does not match sibling tenant", + token: genToken([]string{"/tenant1"}, nil), + method: "GET", + path: "/tenant1234/secret", + isWrite: false, + expectAuthorized: false, + }, + { + name: "tenant prefix does not match dashed sibling", + token: genToken([]string{"/tenant1"}, nil), + method: "GET", + path: "/tenant1-old/secret", + isWrite: false, + expectAuthorized: false, + }, + { + name: "tenant prefix matches own subtree", + token: genToken([]string{"/tenant1"}, nil), + method: "GET", + path: "/tenant1/ok.txt", + isWrite: false, + expectAuthorized: true, + }, + { + name: "tenant prefix with trailing slash matches own subtree", + token: genToken([]string{"/tenant1/"}, nil), + method: "GET", + path: "/tenant1/ok.txt", + isWrite: false, + expectAuthorized: true, + }, + { + name: "root prefix matches anywhere", + token: genToken([]string{"/"}, nil), + method: "GET", + path: "/tenant1234/secret", + isWrite: false, + expectAuthorized: true, + }, + { + name: "dot-dot cannot escape allowed subtree", + token: genToken([]string{"/tenant1"}, nil), + method: "GET", + path: "/tenant1/../tenant2/secret", + isWrite: false, + expectAuthorized: false, + }, } for _, tt := range tests { @@ -151,3 +199,33 @@ func TestFilerServer_maybeCheckJwtAuthorization_Scoped(t *testing.T) { }) } } + +func TestPathHasComponentPrefix(t *testing.T) { + tests := []struct { + name string + path string + prefix string + want bool + }{ + {"exact match", "/tenant1", "/tenant1", true}, + {"subtree match", "/tenant1/a/b", "/tenant1", true}, + {"trailing slash prefix", "/tenant1/a", "/tenant1/", true}, + {"sibling numeric suffix", "/tenant1234/secret", "/tenant1", false}, + {"sibling dash suffix", "/tenant1-old/secret", "/tenant1", false}, + {"sibling dot suffix", "/tenant1.bak/x", "/tenant1", false}, + {"unrelated tree", "/other/x", "/tenant1", false}, + {"root prefix matches root", "/", "/", true}, + {"root prefix matches any", "/tenant1234/secret", "/", true}, + {"empty prefix denies", "/tenant1/x", "", false}, + {"dot-dot does not escape", "/tenant1/../tenant2/secret", "/tenant1", false}, + {"dot-dot stays inside", "/tenant1/a/../b", "/tenant1", true}, + {"double slashes normalised", "/tenant1//a", "/tenant1", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := pathHasComponentPrefix(tt.path, tt.prefix); got != tt.want { + t.Errorf("pathHasComponentPrefix(%q, %q) = %v, want %v", tt.path, tt.prefix, got, tt.want) + } + }) + } +} diff --git a/weed/server/filer_server_handlers.go b/weed/server/filer_server_handlers.go index 8c27244f6..d61e529ac 100644 --- a/weed/server/filer_server_handlers.go +++ b/weed/server/filer_server_handlers.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net/http" + "path" "strconv" "strings" "sync/atomic" @@ -256,7 +257,7 @@ func (fs *FilerServer) maybeCheckJwtAuthorization(r *http.Request, isWrite bool) if len(claims.AllowedPrefixes) > 0 { hasPrefix := false for _, prefix := range claims.AllowedPrefixes { - if strings.HasPrefix(r.URL.Path, prefix) { + if pathHasComponentPrefix(r.URL.Path, prefix) { hasPrefix = true break } @@ -283,6 +284,32 @@ func (fs *FilerServer) maybeCheckJwtAuthorization(r *http.Request, isWrite bool) return true } +// pathHasComponentPrefix reports whether reqPath is contained within the +// directory subtree denoted by prefix, treating both as "/"-separated +// path components. Both inputs are normalised with path.Clean to neutralise +// "." and ".." segments and collapse duplicate slashes. A prefix of "/" +// matches any path. +func pathHasComponentPrefix(reqPath, prefix string) bool { + if prefix == "" { + return false + } + cleanedPath := path.Clean(reqPath) + if cleanedPath == "." { + cleanedPath = "/" + } + cleanedPrefix := path.Clean(prefix) + if cleanedPrefix == "." { + cleanedPrefix = "/" + } + if cleanedPrefix == "/" { + return true + } + if cleanedPath == cleanedPrefix { + return true + } + return strings.HasPrefix(cleanedPath, cleanedPrefix+"/") +} + func (fs *FilerServer) filerHealthzHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Server", "SeaweedFS "+version.VERSION) if _, err := fs.filer.Store.FindEntry(context.Background(), filer.TopicsDir); err != nil && err != filer_pb.ErrNotFound {