mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-05 03:27:42 +01:00
s3: reject POST Object header fields with control characters (#12400)
A Content-Type form field such as "\n" was copied verbatim into the multipart part header sent to the volume server, which broke the framing and surfaced as a Content-MD5 mismatch and a 500. Form fields promoted to request headers must be valid header values, as on a PUT; otherwise answer 400 MalformedPOSTRequest. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0742d316aa
commit
c437791991
2 files changed
+76
No files matched your search
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/policy"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
)
|
||||
|
||||
func (s3a *S3ApiServer) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -145,6 +146,10 @@ func (s3a *S3ApiServer) PostPolicyBucketHandler(w http.ResponseWriter, r *http.R
|
||||
|
||||
// Forward validated POST form fields to the underlying PUT as headers.
|
||||
applyPostPolicyFormHeaders(r, formValues)
|
||||
if !validHeaderValues(r.Header) {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrMalformedPOSTRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Authorize the object like the PUT path.
|
||||
if errCode := s3a.iam.AuthorizeObjectWrite(r, identity, bucket, object); errCode != s3err.ErrNone {
|
||||
@@ -312,6 +317,19 @@ func applyPostPolicyFormHeaders(r *http.Request, formValues http.Header) {
|
||||
}
|
||||
}
|
||||
|
||||
// validHeaderValues rejects form values carrying control characters, which a
|
||||
// real PUT could not send and which would corrupt the volume upload framing.
|
||||
func validHeaderValues(h http.Header) bool {
|
||||
for _, values := range h {
|
||||
for _, v := range values {
|
||||
if !httpguts.ValidHeaderFieldValue(v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// postRequestsAcl reports whether the request carries ACL headers, whether
|
||||
// promoted from the POST form fields or supplied on the request itself.
|
||||
func postRequestsAcl(r *http.Request) bool {
|
||||
|
||||
@@ -733,3 +733,61 @@ func TestPostPolicyBucketHandler_AuthDisabledSkipsSignature(t *testing.T) {
|
||||
assert.Contains(t, rec.Body.String(), "Policy Condition failed",
|
||||
"the policy check must still run, body: %s", rec.Body.String())
|
||||
}
|
||||
|
||||
func TestPostPolicyBucketHandler_RejectsControlCharsInHeaderFields(t *testing.T) {
|
||||
const testBucket = "test-bucket"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
field string
|
||||
value string
|
||||
}{
|
||||
{"content-type newline", "Content-Type", "\n"},
|
||||
{"content-type crlf", "Content-Type", "text/plain\r\nContent-MD5: x"},
|
||||
{"content-disposition newline", "Content-Disposition", "inline\n"},
|
||||
{"metadata newline", "x-amz-meta-foo", "a\nb"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
iam := &IdentityAccessManagement{
|
||||
hashes: make(map[string]*sync.Pool),
|
||||
hashCounters: make(map[string]*int32),
|
||||
}
|
||||
assert.NoError(t, iam.loadS3ApiConfiguration(&iam_pb.S3ApiConfiguration{}))
|
||||
|
||||
s3a := &S3ApiServer{
|
||||
option: &S3ApiServerOption{BucketsPath: "/buckets"},
|
||||
iam: iam,
|
||||
}
|
||||
s3a.bucketRegistry = NewBucketRegistry(s3a)
|
||||
s3a.bucketRegistry.setMetadataCache(&BucketMetaData{Name: testBucket, IsTableBucket: false})
|
||||
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
assert.NoError(t, writer.WriteField("key", "object.txt"))
|
||||
assert.NoError(t, writer.WriteField(tt.field, tt.value))
|
||||
filePart, err := writer.CreateFormFile("file", "payload.txt")
|
||||
assert.NoError(t, err)
|
||||
_, err = filePart.Write([]byte("contents"))
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, writer.Close())
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/"+testBucket, &buf)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req = mux.SetURLVars(req, map[string]string{"bucket": testBucket})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
s3a.PostPolicyBucketHandler(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, rec.Code, "body: %s", rec.Body.String())
|
||||
assert.Contains(t, rec.Body.String(), "MalformedPOSTRequest")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidHeaderValues(t *testing.T) {
|
||||
assert.True(t, validHeaderValues(http.Header{"Content-Type": {"text/plain; charset=utf-8"}, "X-Amz-Meta-Name": {"café\tbar"}}))
|
||||
assert.False(t, validHeaderValues(http.Header{"Content-Type": {"\r\n"}}))
|
||||
assert.False(t, validHeaderValues(http.Header{"Cache-Control": {"no-cache\x00"}}))
|
||||
}
|
||||
Reference in new issue
Block a user