fix: normalize object keys during bucket policy evaluation

Object key validation allowed internal parent-directory segments such as `public/../private.txt`. Bucket policy and auth checks evaluated the raw key, so a policy allowing bucket/public/* could match the request while posix backend later resolved the key with `filepath.Join` and accessed `bucket/private.txt`.

Add backend-specific object key normalization to close that mismatch. The Backend interface now exposes `NormalizeObjectKey` so authorization can evaluate resources using the same key shape a backend will use for storage access.

Backends that do not collapse object paths, including Azure and the S3 proxy, inherit `BackendUnsupported.NormalizeObjectKey`. That implementation returns the input key unchanged, avoiding unnecessary normalization and keeping policy evaluation unpolluted for object stores where ../ is part of the key name.

posix/scoutfs normalize keys with filepath.Join so policy resources and request keys are compared after internal dot segments are collapsed.

Bucket policy evaluation now normalizes both the incoming object key and object resource patterns from the policy before matching. Object lock governance bypass policy checks use the same backend normalizer as well, so retention and legal hold authorization cannot diverge from backend path resolution.
This commit is contained in:
niksis02
2026-05-27 22:20:39 +04:00
parent f4e5df3b0f
commit cd0b4e6d9d
11 changed files with 495 additions and 67 deletions
+50
View File
@@ -131,6 +131,9 @@ var _ backend.Backend = &BackendMock{}
// ListPartsFunc: func(contextMoqParam context.Context, listPartsInput *s3.ListPartsInput) (s3response.ListPartsResult, error) {
// panic("mock out the ListParts method")
// },
// NormalizeObjectKeyFunc: func(bucket string, object string) string {
// panic("mock out the NormalizeObjectKey method")
// },
// PutBucketAclFunc: func(contextMoqParam context.Context, bucket string, data []byte) error {
// panic("mock out the PutBucketAcl method")
// },
@@ -300,6 +303,9 @@ type BackendMock struct {
// ListPartsFunc mocks the ListParts method.
ListPartsFunc func(contextMoqParam context.Context, listPartsInput *s3.ListPartsInput) (s3response.ListPartsResult, error)
// NormalizeObjectKeyFunc mocks the NormalizeObjectKey method.
NormalizeObjectKeyFunc func(bucket string, object string) string
// PutBucketAclFunc mocks the PutBucketAcl method.
PutBucketAclFunc func(contextMoqParam context.Context, bucket string, data []byte) error
@@ -626,6 +632,13 @@ type BackendMock struct {
// ListPartsInput is the listPartsInput argument value.
ListPartsInput *s3.ListPartsInput
}
// NormalizeObjectKey holds details about calls to the NormalizeObjectKey method.
NormalizeObjectKey []struct {
// Bucket is the bucket argument value.
Bucket string
// Object is the object argument value.
Object string
}
// PutBucketAcl holds details about calls to the PutBucketAcl method.
PutBucketAcl []struct {
// ContextMoqParam is the contextMoqParam argument value.
@@ -813,6 +826,7 @@ type BackendMock struct {
lockListObjects sync.RWMutex
lockListObjectsV2 sync.RWMutex
lockListParts sync.RWMutex
lockNormalizeObjectKey sync.RWMutex
lockPutBucketAcl sync.RWMutex
lockPutBucketCors sync.RWMutex
lockPutBucketOwnershipControls sync.RWMutex
@@ -2165,6 +2179,42 @@ func (mock *BackendMock) ListPartsCalls() []struct {
return calls
}
// NormalizeObjectKey calls NormalizeObjectKeyFunc.
func (mock *BackendMock) NormalizeObjectKey(bucket string, object string) string {
if mock.NormalizeObjectKeyFunc == nil {
panic("BackendMock.NormalizeObjectKeyFunc: method is nil but Backend.NormalizeObjectKey was just called")
}
callInfo := struct {
Bucket string
Object string
}{
Bucket: bucket,
Object: object,
}
mock.lockNormalizeObjectKey.Lock()
mock.calls.NormalizeObjectKey = append(mock.calls.NormalizeObjectKey, callInfo)
mock.lockNormalizeObjectKey.Unlock()
return mock.NormalizeObjectKeyFunc(bucket, object)
}
// NormalizeObjectKeyCalls gets all the calls that were made to NormalizeObjectKey.
// Check the length with:
//
// len(mockedBackend.NormalizeObjectKeyCalls())
func (mock *BackendMock) NormalizeObjectKeyCalls() []struct {
Bucket string
Object string
} {
var calls []struct {
Bucket string
Object string
}
mock.lockNormalizeObjectKey.RLock()
calls = mock.calls.NormalizeObjectKey
mock.lockNormalizeObjectKey.RUnlock()
return calls
}
// PutBucketAcl calls PutBucketAclFunc.
func (mock *BackendMock) PutBucketAcl(contextMoqParam context.Context, bucket string, data []byte) error {
if mock.PutBucketAclFunc == nil {