Files
seaweedfs/weed/s3api/s3api_object_handlers_response_headers_test.go
T
Chris Lu 403d51631b s3api: stop echoing stored tags and internal attributes on GET/HEAD (#12053)
* s3api: stop echoing stored tags and internal attributes on GET/HEAD

setResponseHeaders copied every entry.Extended key into the response,
only filtering xattr- and x-seaweedfs-. Stored object tags
(X-Amz-Tagging-*) and the gateway's own attributes (Seaweed-X-Amz-Acl,
-Owner, -Version-Id, -Object-Lock-Mode, ...) therefore leaked to any
caller holding s3:GetObject, and to anonymous readers on public-read
objects, without s3:GetObjectTagging or READ_ACP.

Tag count, user metadata, storage class and the real S3 headers
(x-amz-version-id, x-amz-object-lock-*) are unaffected.

* s3api: match stored tag and internal attribute keys case-insensitively
2026-10-25 06:05:06 -07:00

67 lines
2.7 KiB
Go

package s3api
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
)
// GetObject/HeadObject must expose only response headers AWS defines: user
// metadata, standard entity headers and x-amz-tagging-count. Tag values live
// behind GetObjectTagging and ACL/owner/lock state behind its own APIs; the
// generic Extended echo must not hand them to any caller that can read the
// object.
func TestSetResponseHeadersFiltersInternalExtendedKeys(t *testing.T) {
s3a := &S3ApiServer{}
entry := &filer_pb.Entry{
Name: "object",
Attributes: &filer_pb.FuseAttributes{Mtime: 100},
Extended: map[string][]byte{
s3_constants.AmzObjectTaggingPrefix + "project": []byte("alpha"),
s3_constants.AmzObjectTaggingPrefix + "owner": []byte("y"),
"x-amz-tagging-sneaky": []byte("z"),
"seaweed-x-amz-acl": []byte("z"),
s3_constants.ExtAmzAclKey: []byte(`[{"Permission":"FULL_CONTROL"}]`),
s3_constants.ExtAmzOwnerKey: []byte("admin"),
s3_constants.ExtVersionIdKey: []byte("version-1"),
s3_constants.ExtObjectLockModeKey: []byte("COMPLIANCE"),
s3_constants.ExtETagKey: []byte("d41d8cd98f00b204e9800998ecf8427e"),
s3_constants.SeaweedFSWriteId: []byte("write-id"),
"X-Amz-Meta-Foo": []byte("bar"),
"Cache-Control": []byte("no-cache"),
"xattr-user.note": []byte("n"),
},
}
for _, method := range []string{http.MethodGet, http.MethodHead} {
w := httptest.NewRecorder()
s3a.setResponseHeaders(w, httptest.NewRequest(method, "/bucket/object", nil), entry, 8)
for key := range w.Header() {
lowerKey := strings.ToLower(key)
if lowerKey == s3_constants.AmzTagCount {
continue
}
if strings.HasPrefix(lowerKey, strings.ToLower(s3_constants.ExtAmzPrefix)) {
t.Errorf("%s: internal header %q leaked into response", method, key)
}
if strings.HasPrefix(lowerKey, strings.ToLower(s3_constants.AmzObjectTaggingPrefix)) {
t.Errorf("%s: object tag value %q leaked into response", method, key)
}
}
if got := w.Header().Get(s3_constants.AmzTagCount); got != "2" {
t.Errorf("%s: tag count = %q, want 2", method, got)
}
if got := w.Header()["X-Amz-Meta-foo"]; len(got) != 1 || got[0] != "bar" {
t.Errorf("%s: user metadata dropped, X-Amz-Meta-foo = %v", method, got)
}
if got := w.Header().Get("Cache-Control"); got != "no-cache" {
t.Errorf("%s: Cache-Control = %q", method, got)
}
}
}