mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-25 22:40:44 +01:00
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
This commit is contained in:
1 parent
52d91008ac
commit
403d51631b
2 files changed
+87
-15
No files matched your search
@@ -2401,23 +2401,29 @@ func (s3a *S3ApiServer) setResponseHeaders(w http.ResponseWriter, r *http.Reques
|
||||
// Set custom headers from entry.Extended (user metadata)
|
||||
// Use direct map assignment to preserve original header casing (matches proxy behavior)
|
||||
if entry.Extended != nil {
|
||||
extAmzPrefixLower := strings.ToLower(s3_constants.ExtAmzPrefix)
|
||||
taggingPrefixLower := strings.ToLower(s3_constants.AmzObjectTaggingPrefix)
|
||||
for k, v := range entry.Extended {
|
||||
// Skip internal SeaweedFS headers
|
||||
if !strings.HasPrefix(k, "xattr-") && !s3_constants.IsSeaweedFSInternalHeader(k) {
|
||||
// Support backward compatibility: migrate old non-canonical format to canonical format
|
||||
// OLD: "x-amz-meta-foo" → NEW: "X-Amz-Meta-foo" (preserving suffix case)
|
||||
headerKey := k
|
||||
if len(k) >= 11 && strings.EqualFold(k[:11], "x-amz-meta-") {
|
||||
// Normalize to AWS S3 format: "X-Amz-Meta-" prefix with lowercase suffix
|
||||
// AWS S3 returns user metadata with the suffix in lowercase
|
||||
suffix := k[len("x-amz-meta-"):]
|
||||
headerKey = s3_constants.AmzUserMetaPrefix + strings.ToLower(suffix)
|
||||
if glog.V(4) && k != headerKey {
|
||||
glog.Infof("Normalizing user metadata header %q to %q in response", k, headerKey)
|
||||
}
|
||||
}
|
||||
w.Header()[headerKey] = []string{string(v)}
|
||||
// Skip internal SeaweedFS headers, and the stored tag/ACL/lock
|
||||
// metadata that belongs behind its own API (GetObjectTagging,
|
||||
// GetObjectAcl, GetObjectRetention) instead of every reader.
|
||||
lowerKey := strings.ToLower(k)
|
||||
if strings.HasPrefix(lowerKey, "xattr-") || strings.HasPrefix(lowerKey, extAmzPrefixLower) || strings.HasPrefix(lowerKey, taggingPrefixLower) || s3_constants.IsSeaweedFSInternalHeader(lowerKey) {
|
||||
continue
|
||||
}
|
||||
// Support backward compatibility: migrate old non-canonical format to canonical format
|
||||
// OLD: "x-amz-meta-foo" → NEW: "X-Amz-Meta-foo" (preserving suffix case)
|
||||
headerKey := k
|
||||
if len(k) >= 11 && strings.EqualFold(k[:11], "x-amz-meta-") {
|
||||
// Normalize to AWS S3 format: "X-Amz-Meta-" prefix with lowercase suffix
|
||||
// AWS S3 returns user metadata with the suffix in lowercase
|
||||
suffix := k[len("x-amz-meta-"):]
|
||||
headerKey = s3_constants.AmzUserMetaPrefix + strings.ToLower(suffix)
|
||||
if glog.V(4) && k != headerKey {
|
||||
glog.Infof("Normalizing user metadata header %q to %q in response", k, headerKey)
|
||||
}
|
||||
}
|
||||
w.Header()[headerKey] = []string{string(v)}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user