mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-10 13:58:24 +01:00
s3,iceberg: reject .. in URL path vars (#9687)
* s3,iceberg: reject `..`/NUL in URL path vars Both gateway routers use mux.NewRouter().SkipClean(true), so a request like `GET /bucket-A/../evil-bucket/key` survives routing as bucket=bucket-A, object=../evil-bucket/key. The captured key is then joined into a filer path; util.JoinPath / path.Join collapse the `..` server-side and the read lands in evil-bucket. With auth on, IAM still authorizes against bucket-A (the mux var), so policy is evaluated against the wrong target. Add a middleware on the S3 bucket subrouter and the Iceberg REST router that rejects any `.`, `..`, NUL, or — for single-segment slots — embedded slash in the captured path vars before any handler runs. NormalizeObjectKey already folds `\` to `/` and decoding happens in mux, so `%2e%2e` and `..\` are caught. * s3,iceberg: reject empty captured vars and empty namespace parts Comma-ok the var lookup so we only check captured slots, then treat an empty captured value as a rejection on its own — downstream path.Join would otherwise collapse it and let the next segment pick the bucket. For iceberg, also reject empty parts after splitting the namespace on \x1F so leading/trailing/consecutive unit separators (which parseNamespace silently folds out) don't let distinct route values collapse to the same parsed namespace. Register loggingMiddleware before validateRequestPath on the iceberg router so rejected requests still produce an audit-log line.
This commit is contained in:
1 parent
1355c7a102
commit
dd1b428789
8 files changed
+417
No files matched your search
@@ -89,6 +89,65 @@ func TestNormalizeObjectKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidObjectKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want bool
|
||||
}{
|
||||
{"empty", "", true},
|
||||
{"plain", "folder/file.txt", true},
|
||||
{"leading slash", "/folder/file.txt", true},
|
||||
{"trailing slash", "folder/", true},
|
||||
{"hidden file ok", ".hidden", true},
|
||||
{"dotdot in name ok", "..hidden", true},
|
||||
{"double dots inside name", "foo..bar/baz", true},
|
||||
|
||||
{"bare dotdot", "..", false},
|
||||
{"bare dot", ".", false},
|
||||
{"leading dotdot segment", "../evil-bucket/test.txt", false},
|
||||
{"leading dot-slash", "./evil/test.txt", false},
|
||||
{"nested dotdot segment", "good/../evil/test.txt", false},
|
||||
{"trailing dotdot segment", "good/..", false},
|
||||
{"backslash dotdot", "..\\evil\\test.txt", false},
|
||||
{"mixed-slash dotdot", "good\\..\\evil/test.txt", false},
|
||||
{"dotdot after duplicate slash", "good//../evil", false},
|
||||
{"nul byte", "foo\x00bar", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsValidObjectKey(tt.input); got != tt.want {
|
||||
t.Errorf("IsValidObjectKey(%q) = %v, want %v", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidBucketName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want bool
|
||||
}{
|
||||
{"empty ok", "", true},
|
||||
{"plain", "my-bucket", true},
|
||||
{"name containing dots", "my.bucket.name", true},
|
||||
|
||||
{"bare dot", ".", false},
|
||||
{"bare dotdot", "..", false},
|
||||
{"with slash", "evil/bucket", false},
|
||||
{"with backslash", "evil\\bucket", false},
|
||||
{"with nul", "evil\x00bucket", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsValidBucketName(tt.input); got != tt.want {
|
||||
t.Errorf("IsValidBucketName(%q) = %v, want %v", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveDuplicateSlashes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in new issue
Block a user