Files
seaweedfs/weed/s3api/s3api_ambiguous_subresource_test.go
T
Chris Lu 210afacd12 s3: close list-type / ownership-controls routing mismatch (#11280)
* s3: reject list-type paired with another operation subresource

?list-type=2&ownershipControls= routes to ListObjectsV2 (the list-type
route is registered first) while the IAM action resolver resolves the
ownershipControls selector to s3:GetBucketOwnershipControls. A principal
denied s3:ListBucket but allowed s3:GetBucketOwnershipControls would
therefore list the bucket. list-type selects an operation just like the
other keys in operationSubresources, so add it there and reject the
combination before routing, matching the fix for policy&tagging (#10987).

* s3: resolve list-type to s3:ListBucket ahead of bucket subresources

The router registers the ListObjectsV2 route ahead of the bucket
subresource routes, so the action resolver should resolve list-type the
same way. Without this, a request carrying list-type and another operation
selector resolves to the subresource action (e.g. s3:GetBucketOwnershipControls)
while being served by ListObjectsV2. The ambiguity guard rejects such
combinations before routing, but resolving list-type to s3:ListBucket keeps
the resolver aligned with the router, mirroring how versions is handled.

* s3: match list-type=2 exactly in action resolver

The router selects ListObjectsV2 only for list-type=2; other values fall
through to the subresource routes. Resolve the same way so the action
matches the handler for every list-type value, not just 2.
2026-09-11 22:21:58 -07:00

104 lines
3.6 KiB
Go

package s3api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestAmbiguousSubresource pins the rule that a request may name only one
// operation. The router picks a handler by registration order and the IAM action
// resolver picks an action by its own order, so a request carrying two operation
// subresources gets authorized as one and served as the other.
func TestAmbiguousSubresource(t *testing.T) {
for _, query := range []string{
"",
"policy=",
"tagging=",
"acl=&versionId=abc",
"tagging=&versionId=abc",
"retention=&versionId=abc",
"uploadId=xyz&partNumber=3",
"attributes=&partNumber=3&versionId=abc",
"versions=&prefix=a&delimiter=/",
"uploads=&prefix=a&x-id=CreateMultipartUpload",
"list-type=2&prefix=a&continuation-token=x",
"acl=&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=deadbeef",
} {
req, _ := http.NewRequest("GET", "http://localhost/bucket/key?"+query, nil)
assert.False(t, hasAmbiguousSubresource(req.URL.Query()), "%q names one operation", query)
}
for _, query := range []string{
"policy=&tagging=",
"tagging=&policy=",
"cors=&tagging=",
"lifecycle=&tagging=",
"versioning=&tagging=",
"object-lock=&tagging=",
"requestPayment=&tagging=",
"acl=&policy=",
"policy=&cors=",
"delete=&policy=",
"uploads=&uploadId=xyz",
"policy=&tagging=&cors=",
"list-type=2&ownershipControls=",
"list-type=2&tagging=",
"ownershipControls=&list-type=2",
"list-type=2&versions=",
} {
req, _ := http.NewRequest("PUT", "http://localhost/bucket?"+query, nil)
assert.True(t, hasAmbiguousSubresource(req.URL.Query()), "%q names two operations", query)
}
}
// The bucket tagger's escalation: PUT /bucket?policy&tagging routes to the
// bucket-policy handler while resolving as s3:PutBucketTagging. The guard has to
// reject it before either the handler or the IAM check runs.
func TestAmbiguousSubresourceRejectedBeforeHandler(t *testing.T) {
served := false
handler := validateRequestPath(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
served = true
}))
req, _ := http.NewRequest("PUT", "http://localhost/bucket?policy=&tagging=", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
require.False(t, served, "an ambiguous request must not reach a handler")
assert.Equal(t, http.StatusBadRequest, rec.Code)
served = false
req, _ = http.NewRequest("PUT", "http://localhost/bucket?policy=", nil)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.True(t, served, "an unambiguous request must still be served")
}
// The listing disclosure: GET /bucket?list-type=2&ownershipControls= routes to
// ListObjectsV2 while resolving as s3:GetBucketOwnershipControls, so a principal
// denied s3:ListBucket but allowed the ownership-controls read would list the
// bucket. The guard has to reject the combined request before the listing handler.
func TestListTypeOwnershipControlsRejectedBeforeHandler(t *testing.T) {
served := false
handler := validateRequestPath(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
served = true
}))
req, _ := http.NewRequest("GET", "http://localhost/bucket?list-type=2&ownershipControls=", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
require.False(t, served, "a list-type+ownershipControls request must not reach a handler")
assert.Equal(t, http.StatusBadRequest, rec.Code)
served = false
req, _ = http.NewRequest("GET", "http://localhost/bucket?list-type=2&prefix=a", nil)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.True(t, served, "a plain list-type request must still be served")
}