Files
versitygw/s3api/utils/name_validate_test.go
niksis02 416acd3863 fix: keep object keys literal under fiber v3.6 path normalization
fiber `v3.6.0` (gofiber/fiber#4695) normalizes every request path before routing as RFC 3986 Section 6.2.2 describes: it decodes the escapes of unreserved characters and removes `.` and `..` segments, and `ctx.Path()`, `ctx.Params()` and every `ctx.Path(override)` return the normalized path with no way to opt out. `DecodeURL` sets the decoded path back through `ctx.Path()`, so keys were decoded twice (`100%2541` was stored as `100A`, `q%7euser` as `q~user`) and dot segments rerouted requests: `PUT /bucket/.` became `CreateBucket` and `/bucket/../../etc/passwd` named the bucket `etc`.

The new `httpctx.Ctx`, created with `fiber.NewWithCustomCtx`, keeps a path set with `Path()` as it is and has fiber route a copy in which every `%` and the dots of a `.` or `..` segment are `_`. The copy keeps the length of the path, since fiber matches routes on it but takes the route parameters from `Path()` at the matched offsets, so `Params("bucket")` holds the real bucket. The S3, admin and website servers and the RDMA ops tracker use it.

S3 stores keys holding `.` and `..` segments as they are (`a/../b`, `x/.` and `..` are objects of their own) and only rejects, before authentication, a request path whose decoded `..` segments climb above the root, separating the segments at `\` as well as at `/` and skipping the empty ones. `DecodeURL` now rejects such a path with `InvalidURI` and keeps the dot segments in the key otherwise.

`Backend.StoresDotSegmentKeys()` reports whether a backend stores such keys as their own objects: `azure` and `s3proxy` do, while `posix` and the `BackendUnsupported` default don't, since a filesystem resolves the segments to another object or to a path outside the bucket. `IsObjectNameValid` takes the backend and rejects keys holding dot segments for those backends in the request path, the `DeleteObjects` keys, the `PostObject` key and the copy sources, so `posix` now also rejects `a/../b` and `a/./b`, which it stored as `b` and `a/b`. The unused `path.go` is removed.
2026-10-20 22:02:15 +04:00

72 lines
2.4 KiB
Go

// Copyright 2025 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package utils
import (
"testing"
"github.com/versity/versitygw/backend"
)
func TestIsObjectNameValid(t *testing.T) {
tests := []struct {
name string
input string
want bool // for a backend storing objects as files
wantDotKeys bool // for a backend storing dot segment keys
}{
{"simple file", "file.txt", true, true},
{"nested file", "dir/file.txt", true, true},
{"absolute nested file", "/dir/file.txt", true, true},
{"trailing slash", "dir/", true, true},
{"slash prefix", "/file.txt", true, true},
{"leading dot", ".hidden", true, true},
{"three dots", "...", true, true},
{"dots inside segments", "a..b/c.", true, true},
{"dot slash prefix", "./file.txt", false, true},
{"dot only", ".", false, true},
{"dot dot only", "..", false, true},
{"dot slash", "./", false, true},
{"dot segment", "a/./b", false, true},
{"dot dot segment", "a/../b", false, true},
{"trailing dot dot", "dir/..", false, true},
{"escapes the bucket", "../file.txt", false, true},
{"escapes the bucket deeper", "dir/../../../file.txt", false, true},
{"empty", "", false, false},
{"slash only", "/", false, false},
{"slashes only", "//", false, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsObjectNameValid(tt.input, backend.New()); got != tt.want {
t.Errorf("IsObjectNameValid(%q) = %v, want %v", tt.input, got, tt.want)
}
if got := IsObjectNameValid(tt.input, dotSegmentBackend{}); got != tt.wantDotKeys {
t.Errorf("IsObjectNameValid(%q) with dot segment keys = %v, want %v",
tt.input, got, tt.wantDotKeys)
}
})
}
}
// dotSegmentBackend stores keys holding "." and ".." segments, like the
// azure and s3proxy backends.
type dotSegmentBackend struct {
backend.BackendUnsupported
}
func (dotSegmentBackend) StoresDotSegmentKeys() bool { return true }