From 89aa822a4055869891953cf07e63fdb9250c015a Mon Sep 17 00:00:00 2001 From: niksis02 Date: Wed, 11 Feb 2026 16:45:36 +0400 Subject: [PATCH] fix: fixes DeleteObject if-match quoted comparison Fixes #1835 If-Match in DeleteObject is a precondition header that compares the client-provided ETag with the server-side ETag before deleting the object. Previously, the comparison failed when the client sent an unquoted ETag, because server ETags are stored with quotes. The implementation now trims quotes from both the input ETag and the server ETag before comparison to avoid mismatches. Both quoted and unquoted ETags are valid according to S3. --- backend/common.go | 1 + s3api/controllers/object-delete.go | 2 +- s3api/utils/precondition.go | 6 +++--- tests/integration/DeleteObject.go | 9 +++++++++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/backend/common.go b/backend/common.go index 44bd0d9..b9ae2e1 100644 --- a/backend/common.go +++ b/backend/common.go @@ -634,6 +634,7 @@ type ObjectDeletePreconditions struct { // EvaluateObjectDeletePreconditions evaluates preconditions for DeleteObject func EvaluateObjectDeletePreconditions(etag string, modTime time.Time, size int64, preconditions ObjectDeletePreconditions) error { + etag = strings.Trim(etag, `"`) ifMatch := preconditions.IfMatch if ifMatch != nil && *ifMatch != etag { return errPreconditionFailed diff --git a/s3api/controllers/object-delete.go b/s3api/controllers/object-delete.go index bac9d45..3a3b6ee 100644 --- a/s3api/controllers/object-delete.go +++ b/s3api/controllers/object-delete.go @@ -133,7 +133,7 @@ func (c S3ApiController) DeleteObject(ctx *fiber.Ctx) (*Response, error) { key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket)) versionId := ctx.Query("versionId") bypass := strings.EqualFold(ctx.Get("X-Amz-Bypass-Governance-Retention"), "true") - ifMatch := utils.GetStringPtr(ctx.Get("If-Match")) + ifMatch := utils.GetStringPtr(utils.TrimQuotes(ctx.Get("If-Match"))) ifMatchLastModTime := utils.ParsePreconditionDateHeader(ctx.Get("X-Amz-If-Match-Last-Modified-Time")) ifMatchSize := utils.ParseIfMatchSize(ctx) // context locals diff --git a/s3api/utils/precondition.go b/s3api/utils/precondition.go index 0852980..52a857b 100644 --- a/s3api/utils/precondition.go +++ b/s3api/utils/precondition.go @@ -68,8 +68,8 @@ func ParsePreconditionMatchHeaders(ctx *fiber.Ctx, opts ...preconditionOpt) (*st prefix = "X-Amz-Copy-Source-" } - ifMatch := trimQuotes(ctx.Get(prefix + "If-Match")) - ifNoneMatch := trimQuotes(ctx.Get(prefix + "If-None-Match")) + ifMatch := TrimQuotes(ctx.Get(prefix + "If-Match")) + ifNoneMatch := TrimQuotes(ctx.Get(prefix + "If-None-Match")) return GetStringPtr(ifMatch), GetStringPtr(ifNoneMatch) } @@ -143,7 +143,7 @@ func ParseIfMatchSize(ctx *fiber.Ctx) *int64 { return &ifMatchSize } -func trimQuotes(str string) string { +func TrimQuotes(str string) string { if len(str) < 2 { return str } diff --git a/tests/integration/DeleteObject.go b/tests/integration/DeleteObject.go index 95acfe1..8c9a0fd 100644 --- a/tests/integration/DeleteObject.go +++ b/tests/integration/DeleteObject.go @@ -19,6 +19,7 @@ import ( "context" "fmt" "net/http" + "strings" "time" "github.com/aws/aws-sdk-go-v2/service/s3" @@ -124,6 +125,7 @@ func DeleteObject_conditional_writes(s *S3Conf) error { return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { obj := "my-obj" var etag *string = getPtr("") + var etagUnquoted *string = getPtr("") var size *int64 = getPtr(int64(0)) var modTime *time.Time = getPtr(time.Now()) @@ -149,6 +151,9 @@ func DeleteObject_conditional_writes(s *S3Conf) error { } *etag = *res.res.ETag + if etag != nil { + *etagUnquoted = strings.Trim(*etag, `"`) + } *size = *res.res.Size *modTime = *out.LastModified @@ -176,12 +181,16 @@ func DeleteObject_conditional_writes(s *S3Conf) error { {etag, size, nil, nil}, {etag, nil, modTime, nil}, {nil, size, modTime, nil}, + // unqoted etag + {etagUnquoted, nil, nil, nil}, // error cases {getPtr("incorrect_etag"), nil, nil, errPrecond}, {nil, getPtr(int64(23234)), nil, errPrecond}, {nil, nil, getPtr(time.Now().AddDate(-1, -1, -1)), errPrecond}, {getPtr("incorrect_etag"), getPtr(int64(23234)), nil, errPrecond}, {getPtr("incorrect_etag"), getPtr(int64(23234)), getPtr(time.Now().AddDate(-1, -1, -1)), errPrecond}, + // quoted incorrect etag + {getPtr(`"incorrect_etag"`), nil, nil, errPrecond}, } { err := createObj() if err != nil {