Files
versitygw/s3api/controllers/bucket-post.go
T
niksis02 eecc1a779c fix: reject invalid PostObject keys
Validate multipart PostObject key fields with the existing object name rules so path traversal and degenerate names return BadRequest. This prevents crafted object keys from escaping the gateway root.
2026-05-22 14:48:31 +04:00

304 lines
9.1 KiB
Go

// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package controllers
import (
"encoding/xml"
"net/http"
"net/url"
"strings"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/debuglogger"
"github.com/versity/versitygw/s3api/middlewares"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3event"
"github.com/versity/versitygw/s3response"
)
func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) (*Response, error) {
bucket := ctx.Params("bucket")
bypass := strings.EqualFold(ctx.Get("X-Amz-Bypass-Governance-Retention"), "true")
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
err := auth.VerifyAccess(ctx.Context(), c.be,
auth.AccessOptions{
Readonly: c.readonly,
Acl: parsedAcl,
AclPermission: auth.PermissionWrite,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Actions: []auth.Action{auth.DeleteObjectAction},
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
var dObj s3response.DeleteObjects
err = xml.Unmarshal(ctx.Body(), &dObj)
if err != nil {
debuglogger.Logf("error unmarshalling delete objects: %v", err)
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetAPIError(s3err.ErrInvalidRequest)
}
err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, dObj.Objects, bypass, IsBucketPublic, c.be, false)
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
res, err := c.be.DeleteObjects(ctx.Context(),
&s3.DeleteObjectsInput{
Bucket: &bucket,
Delete: &types.Delete{
Objects: dObj.Objects,
},
})
return &Response{
Data: res,
MetaOpts: &MetaOptions{
ObjectCount: int64(len(dObj.Objects)),
BucketOwner: parsedAcl.Owner,
EventName: s3event.EventObjectRemovedDeleteObjects,
},
}, err
}
func (c S3ApiController) POSTObject(ctx *fiber.Ctx) (*Response, error) {
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
parsed := utils.ContextKeyObjectPostResult.Get(ctx).(middlewares.PostObjectResult)
bucket := ctx.Params("bucket")
contentType := parsed.Fields["content-type"]
contentEncoding := parsed.Fields["content-encoding"]
contentDisposition := parsed.Fields["content-disposition"]
contentLanguage := parsed.Fields["content-language"]
cacheControl := parsed.Fields["cache-control"]
expires := parsed.Fields["expires"]
key := parsed.Fields["key"]
err := auth.VerifyAccess(ctx.Context(), c.be,
auth.AccessOptions{
Readonly: c.readonly,
Acl: parsedAcl,
AclPermission: auth.PermissionWrite,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Actions: []auth.Action{auth.PutObjectAction},
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
// parse POST policy — absent for anonymous uploads to public buckets
if !IsBucketPublic {
policyBase64 := parsed.Fields["policy"]
policy, err := auth.ParsePOSTPolicyBase64(policyBase64)
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
// Evaluate post policy
err = policy.Evaluate(auth.PostPolicyEvalInput{
Bucket: bucket,
Key: key,
ContentLength: parsed.ContentLength,
Fields: parsed.Fields,
})
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
}
// convert object tagging from raw XML to Query string
// to pass PutObject, which expects the tagging to be a query string
var tagging string
if taggingXML, ok := parsed.Fields["tagging"]; ok {
tagging, err = utils.ConvertTaggingXMLToQueryString([]byte(taggingXML))
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
}
// parse checksum headers
checksums, err := utils.ParseCalculatedChecksumFields(parsed.Fields)
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
// extract metadata
metadata, err := utils.ExtractMetadataFromFields(parsed.Fields)
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
res, err := c.be.PutObject(ctx.Context(), s3response.PutObjectInput{
Bucket: &bucket,
Key: &key,
ContentType: &contentType,
ContentEncoding: &contentEncoding,
ContentDisposition: &contentDisposition,
ContentLanguage: &contentLanguage,
CacheControl: &cacheControl,
Expires: &expires,
Body: parsed.FileRdr,
ContentLength: &parsed.ContentLength,
Tagging: &tagging,
Metadata: metadata,
ChecksumCRC32: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32]),
ChecksumCRC32C: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32c]),
ChecksumSHA1: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha1]),
ChecksumSHA256: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha256]),
ChecksumCRC64NVME: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc64nvme]),
ChecksumSHA512: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha512]),
ChecksumMD5: utils.GetStringPtr(checksums[types.ChecksumAlgorithmMd5]),
ChecksumXXHASH64: utils.GetStringPtr(checksums[types.ChecksumAlgorithmXxhash64]),
ChecksumXXHASH3: utils.GetStringPtr(checksums[types.ChecksumAlgorithmXxhash3]),
ChecksumXXHASH128: utils.GetStringPtr(checksums[types.ChecksumAlgorithmXxhash128]),
})
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
if successActionRedirect, ok := parsed.Fields["success_action_redirect"]; ok {
u, err := url.Parse(successActionRedirect)
if err == nil {
q := u.Query()
q.Set("bucket", bucket)
q.Set("key", key)
q.Set("etag", res.ETag)
u.RawQuery = q.Encode()
redirectURI := u.String()
return &Response{
Headers: map[string]*string{
"Location": &redirectURI,
},
MetaOpts: &MetaOptions{
ContentLength: parsed.FileRdr.Length(),
BucketOwner: parsedAcl.Owner,
ObjectETag: &res.ETag,
ObjectSize: parsed.FileRdr.Length(),
EventName: s3event.EventObjectCreatedPost,
Status: http.StatusSeeOther,
},
}, nil
}
}
respStatus := http.StatusNoContent
var respBody any
location := utils.GenerateObjectLocation(ctx, c.virtualDomain, bucket, key)
if successStatus, ok := parsed.Fields["success_action_status"]; ok {
switch successStatus {
case "200":
respStatus = http.StatusOK
case "201":
respStatus = http.StatusCreated
respBody = &s3response.PostResponse{
Bucket: bucket,
Key: key,
ETag: res.ETag,
Location: location,
}
}
}
return &Response{
Headers: map[string]*string{
"Etag": &res.ETag,
"Location": &location,
"x-amz-checksum-crc32": res.ChecksumCRC32,
"x-amz-checksum-crc32c": res.ChecksumCRC32C,
"x-amz-checksum-crc64nvme": res.ChecksumCRC64NVME,
"x-amz-checksum-sha1": res.ChecksumSHA1,
"x-amz-checksum-sha256": res.ChecksumSHA256,
"x-amz-checksum-sha512": res.ChecksumSHA512,
"x-amz-checksum-md5": res.ChecksumMD5,
"x-amz-checksum-xxhash64": res.ChecksumXXHASH64,
"x-amz-checksum-xxhash3": res.ChecksumXXHASH3,
"x-amz-checksum-xxhash128": res.ChecksumXXHASH128,
"x-amz-checksum-type": utils.ConvertToStringPtr(res.ChecksumType),
"x-amz-version-id": utils.GetStringPtr(res.VersionID),
},
Data: respBody,
MetaOpts: &MetaOptions{
ContentLength: parsed.FileRdr.Length(),
BucketOwner: parsedAcl.Owner,
ObjectETag: &res.ETag,
ObjectSize: parsed.FileRdr.Length(),
EventName: s3event.EventObjectCreatedPost,
Status: respStatus,
},
}, nil
}