diff --git a/s3api/middlewares/authentication.go b/s3api/middlewares/authentication.go index b9afa33f..007a30a6 100644 --- a/s3api/middlewares/authentication.go +++ b/s3api/middlewares/authentication.go @@ -115,6 +115,9 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string, } hashPayload := ctx.Get("X-Amz-Content-Sha256") + if !utils.IsValidSh256PayloadHeader(hashPayload) { + return s3err.GetAPIError(s3err.ErrInvalidSHA256Paylod) + } if utils.IsBigDataAction(ctx) { // for streaming PUT actions, authorization is deferred // until end of stream due to need to get length and diff --git a/s3api/utils/chunk-reader.go b/s3api/utils/chunk-reader.go index bba83fdb..7af15f78 100644 --- a/s3api/utils/chunk-reader.go +++ b/s3api/utils/chunk-reader.go @@ -15,6 +15,7 @@ package utils import ( + "encoding/hex" "fmt" "io" "net/http" @@ -105,6 +106,28 @@ func IsSpecialPayload(str string) bool { return specialValues[payloadType(str)] } +// IsValidSh256PayloadHeader checks if the provided x-amz-content-sha256 +// paylod header is valid special paylod type or a valid sh256 hash +func IsValidSh256PayloadHeader(value string) bool { + // empty header is valid + if value == "" { + return true + } + // special values are valid + if specialValues[payloadType(value)] { + return true + } + + // check to be a valid sha256 + if len(value) != 64 { + return false + } + + // decode the string as hex + _, err := hex.DecodeString(value) + return err == nil +} + // Checks if the provided string is unsigned payload trailer type func IsUnsignedStreamingPayload(str string) bool { return payloadType(str) == payloadTypeStreamingUnsignedTrailer diff --git a/s3api/utils/chunk-reader_test.go b/s3api/utils/chunk-reader_test.go new file mode 100644 index 00000000..b0f8d26f --- /dev/null +++ b/s3api/utils/chunk-reader_test.go @@ -0,0 +1,43 @@ +// Copyright 2024 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 utils + +import "testing" + +func TestIsValidSh256PayloadHeader(t *testing.T) { + tests := []struct { + name string + hash string + want bool + }{ + {"empty header", "", true}, + {"special payload type 1", "UNSIGNED-PAYLOAD", true}, + {"special payload type 2", "STREAMING-UNSIGNED-PAYLOAD-TRAILER", true}, + {"special payload type 3", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD", true}, + {"special payload type 4", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER", true}, + {"special payload type 5", "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD", true}, + {"special payload type 6", "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER", true}, + {"invalid hext", "invalid_hex", false}, + {"valid hex, but not sha256", "d41d8cd98f00b204e9800998ecf8427e", false}, + {"valid sh256", "9c56cc51b374bb0f2f8d55af2b34d2a6f8f7f42dd4bbcccbbf8e3279b6e1e6d4", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsValidSh256PayloadHeader(tt.hash); got != tt.want { + t.Errorf("IsValidSh256PayloadHeader() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/s3err/s3err.go b/s3err/s3err.go index 71d3f220..8c7463af 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -125,6 +125,7 @@ const ( ErrSignatureTerminationStr ErrSignatureIncorrService ErrContentSHA256Mismatch + ErrInvalidSHA256Paylod ErrMissingContentLength ErrInvalidAccessKeyID ErrRequestNotReadyYet @@ -530,6 +531,11 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "The provided 'x-amz-content-sha256' header does not match what was computed.", HTTPStatusCode: http.StatusBadRequest, }, + ErrInvalidSHA256Paylod: { + Code: "InvalidArgument", + Description: "x-amz-content-sha256 must be UNSIGNED-PAYLOAD, STREAMING-UNSIGNED-PAYLOAD-TRAILER, STREAMING-AWS4-HMAC-SHA256-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER or a valid sha256 value.", + HTTPStatusCode: http.StatusBadRequest, + }, ErrMissingContentLength: { Code: "MissingContentLength", Description: "You must provide the Content-Length HTTP header.", diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 458f5d34..c3e281cd 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -31,6 +31,7 @@ func TestAuthentication(s *S3Conf) { Authentication_invalid_date_header(s) Authentication_date_mismatch(s) Authentication_incorrect_payload_hash(s) + Authentication_invalid_sha256_payload_hash(s) Authentication_incorrect_md5(s) Authentication_signature_error_incorrect_secret_key(s) } @@ -904,6 +905,7 @@ func GetIntTests() IntTests { "Authentication_invalid_date_header": Authentication_invalid_date_header, "Authentication_date_mismatch": Authentication_date_mismatch, "Authentication_incorrect_payload_hash": Authentication_incorrect_payload_hash, + "Authentication_invalid_sha256_payload_hash": Authentication_invalid_sha256_payload_hash, "Authentication_incorrect_md5": Authentication_incorrect_md5, "Authentication_signature_error_incorrect_secret_key": Authentication_signature_error_incorrect_secret_key, "PresignedAuth_unsupported_algorithm": PresignedAuth_unsupported_algorithm, diff --git a/tests/integration/tests.go b/tests/integration/tests.go index 1380bbb5..d52beb25 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -462,16 +462,41 @@ func Authentication_date_mismatch(s *S3Conf) error { }) } +func Authentication_invalid_sha256_payload_hash(s *S3Conf) error { + testName := "Authentication_invalid_sha256_payload_hash" + return authHandler(s, &authConfig{ + testName: testName, + method: http.MethodPut, + body: nil, + service: "s3", + date: time.Now(), + path: "bucket/object", + }, func(req *http.Request) error { + req.Header.Set("X-Amz-Content-Sha256", "invalid_sha256") + resp, err := s.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidSHA256Paylod)); err != nil { + return err + } + + return nil + }) +} + func Authentication_incorrect_payload_hash(s *S3Conf) error { testName := "Authentication_incorrect_payload_hash" return authHandler(s, &authConfig{ testName: testName, - method: http.MethodGet, + method: http.MethodPut, body: nil, service: "s3", date: time.Now(), + path: "bucket/object?tagging", }, func(req *http.Request) error { - req.Header.Set("X-Amz-Content-Sha256", "7sa6df576dsa5f675sad67f") + req.Header.Set("X-Amz-Content-Sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b854") resp, err := s.httpClient.Do(req) if err != nil {