mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-15 19:56:39 +00:00
filer: scope JWT allowed_prefixes to path components (#9439)
The allowed_prefixes check used a literal byte-prefix match, so a token scoped to /tenant1 also matched /tenant1234, /tenant1-old, and similar sibling paths. Match on /-separated path components after path.Clean normalisation instead.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user