Files
versitygw/s3api/middlewares/checksum.go
niksis02 12fb5a6594 fix: reject empty Content-MD5 on PUT operations
Fixes #2179

Treat a present but empty `Content-MD5` header as an invalid digest instead of handling it as if the header were absent. This makes PUT operations return `InvalidDigest` for empty `Content-MD5` values while preserving existing behavior for missing headers.
2026-06-15 18:53:42 +04:00

128 lines
3.2 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 middlewares
import (
"bytes"
"encoding/base64"
"io"
"strings"
"github.com/gofiber/fiber/v3"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
)
// VerifyChecksums parses, validates, and calculates the
// Content-MD5 and x-amz-checksum-* headers.
// Additionally, it ensures that the request body is not empty
// for actions that require a non-empty body. For large data actions(PutObject, UploadPart),
// it wraps the body reader to handle Content-MD5:
// the x-amz-checksum-* headers are explicitly processed by the backend.
func VerifyChecksums(streamBody bool, requireBody bool, requireChecksum bool) fiber.Handler {
return func(ctx fiber.Ctx) error {
md5Header := ctx.Request().Header.Peek("Content-Md5")
hasMD5Header := md5Header != nil
md5sum := string(md5Header)
if hasMD5Header && md5sum == "" {
return s3err.GetInvalidDigestErr(md5sum)
}
if streamBody {
// for large data actions(PutObject, UploadPart)
// only stack the md5 reader,as x-amz-checksum-*
// calculation is explicitly handled in back-end
if !hasMD5Header {
return nil
}
if !isValidMD5(md5sum) {
return s3err.GetInvalidDigestErr(md5sum)
}
var err error
wrapBodyReader(ctx, func(r io.Reader) io.Reader {
r, err = utils.NewHashReader(r, md5sum, utils.HashTypeContentMD5)
return r
})
if err != nil {
return err
}
return nil
}
body := ctx.BodyRaw()
if requireBody && len(body) == 0 {
return s3err.GetAPIError(s3err.ErrMissingRequestBody)
}
var rdr io.Reader
var err error
if md5sum != "" {
if !isValidMD5(md5sum) {
return s3err.GetInvalidDigestErr(md5sum)
}
rdr, err = utils.NewHashReader(bytes.NewReader(body), md5sum, utils.HashTypeContentMD5)
if err != nil {
return err
}
}
// parse and validate checksum headers
algo, checksums, err := utils.ParseChecksumHeadersAndSdkAlgo(ctx)
if err != nil {
return err
}
if algo != "" {
r, err := utils.NewHashReader(bytes.NewReader(body), checksums[algo], utils.HashType(strings.ToLower(string(algo))))
if err != nil {
return err
}
if rdr != nil {
// combine both md5 and the checksum readers
rdr = io.MultiReader(rdr, r)
} else {
rdr = r
}
}
if rdr == nil && requireChecksum {
return s3err.GetAPIError(s3err.ErrChecksumRequired)
}
if rdr != nil {
_, err = io.Copy(io.Discard, rdr)
if err != nil {
return err
}
}
return nil
}
}
func isValidMD5(s string) bool {
decoded, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return false
}
return len(decoded) == 16
}