Repository: minio/minio (language: go)

Issue:
Uploading the part of a multipart upload with partnumber = 0 gives Success (HTTP 200) + eTag
## NOTE
While playing around with s3 multipart I noticed that (through a bug in my code) i created a presigned url to upload a multipart upload part with a partnumber = 0.

And it happens what minio responded with http 200 (OK) and a proper eTag.

Trying to list the parts using the list-parts doesn't return that uploaded part nor can I complete the multipart-upload using the given eTag. While the multipart upload itself exists I could see a corresponding data file at the windows working dir of ## NOTE
While playing around with s3 multipart I noticed that due to a bug in my code I created a presigned url to upload a multipart upload part with a partnumber = 0.

Nevertheless minio answered the request with http 200 (OK) and an eTag.

When I try to list the parts with list-parts, I don't get back the uploaded part, nor can I complete the multipart upload with the specified eTag.
While the multipart upload exists, I could see a corresponding data file in minio's Windows working directory
(.minio.sys\multipart\8a6756162db131701ef5818f7e32ffc12ff94b947a4ab4ee95095bf8be279200\b35cf7e6-510a-45c4-8fa3-cc31806ac22b\2fe4edf8-eee3-42f3-ad4f-8acf0a54a853\part.0 and part.0.meta).

The aws documentation states, "Part numbers can be any number from 1 to 10,000, inclusive. A part number uniquely identifies a part and also defines its position within the object being created. If you upload a new part with the same part number that was used for a previous part, the previously uploaded part will be overwritten."
Accordingly, I would have expected an error message indicating that I used an incorrect part number.

With the aws cli the behavior is the same as with the generated predefined URL.

I also tested with partnumber = x - then minio correctly displays an error:
`ValueError: invalid literal for int() with base 10: 'x'`.

With partnumber = -1 it also answered with success, but I could not find any file in the local filesystem. (so similar to the case partnumber = 0)

## Expected Behavior
I would expect an error message.
For example, this is what aws s3 replied:
```
<?xml version="1.0" encoding="UTF-8"?>
<Error>
	<Code>InvalidArgument</Code>
	<Message>Part number must be an integer between 1 and 10000, inclusive</Message>
	<ArgumentName>partNumber</ArgumentName>
	<ArgumentValue>0</ArgumentValue>
	<RequestId>KGFA6F9HK7NKCDPN</RequestId>
	<HostId>9IU***Kg=</HostId>
</Error>
```
```
PS E:\temp\aws-cli> aws --no-verify-ssl --profile default s3api create-multipart-upload --bucket s3-MYBUCKET-test --key whatever
urllib3\connectionpool.py:1045: InsecureRequestWarning: Unverified HTTPS request is being made to host 's3-MYBUCKET-test.s3.eu-central-1.amazonaws.com'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html#ssl-warnings
{
    "ServerSideEncryption": "AES256",
    "Bucket": "s3-MYBUCKET-test",
    "Key": "whatever",
    "UploadId": "qAN***S1I-"
}
```
```
PS E:\temp\aws-cli> aws --no-verify-ssl --profile default s3api upload-part --bucket s3-MYBUCKET-test --key whatever --part-number 0 --upload-id qAN***S1I- --body data1.png
urllib3\connectionpool.py:1045: InsecureRequestWarning: Unverified HTTPS request is being made to host 's3-MYBUCKET-test.s3.eu-central-1.amazonaws.com'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html#ssl-warnings

An error occurred (InvalidArgument) when calling the UploadPart operation: Part number must be an integer between 1 and 10000, inclusive
```
## Current Behavior
No error is indicated - instead I get an http success code:
`DEBUG - http://127.0.0.1:9060 "PUT /s3-MYBUCKET-test/whatever?partNumber=0&uploadId=ZmE0NzMzMGYtNGNhNy00ZjM5LTk3NDgtYzNhMWFhZDUwZjFlLjEwMTUyODVkLWUxMDQtNDllMS04NTBmLWZhNDU0MmJkNjgxNA HTTP/1.1" 200 0`

## Steps to Reproduce (for bugs)

1.
```
PS E:\temp\aws-cli> aws --no-verify-ssl --profile minio --endpoint-url http://127.0.0.1:9060 s3api create-multipart-upload --bucket s3-MYBUCKET-test --key whatever
{
    "Bucket": "s3-MYBUCKET-test",
    "Key": "whatever",
    "UploadId": "ZmE0NzMzMGYtNGNhNy00ZjM5LTk3NDgtYzNhMWFhZDUwZjFlLjEwMTUyODVkLWUxMDQtNDllMS04NTBmLWZhNDU0MmJkNjgxNA"
}
```

2.
```
PS E:\temp\aws-cli> aws --no-verify-ssl --profile minio --endpoint-url http://127.0.0.1:9060 s3api upload-part --bucket s3-MYBUCKET-test --key whatever --part-number 0 --upload-id ZmE0NzMzMGYtNGNhNy00ZjM5LTk3NDgtYzNhMWFhZDUwZjFlLjEwMTUyODVkLWUxMDQtNDllMS04NTBmLWZhNDU0MmJkNjgxNA --body data1.png
{
    "ETag": "\"564b3c91b52897c69bbaba0e3415fb4c\""
}
```

3.
```
PS E:\temp\aws-cli> aws --no-verify-ssl --profile minio --endpoint-url http://127.0.0.1:9060 s3api list-parts --bucket s3-MYBUCKET-test --key whatever --upload-id ZmE0NzMzMGYtNGNhNy00ZjM5LTk3NDgtYzNhMWFhZDUwZjFlLjEwMTUyODVkLWUxMDQtNDllMS04NTBmLWZhNDU0MmJkNjgxNA
{
    "ChecksumAlgorithm": "",
    "Initiator": {
        "ID": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4",
        "DisplayName": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
    },
    "Owner": {
        "DisplayName": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4",
        "ID": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
    },
    "StorageClass": "STANDARD"
}
```

## Context
I've tried to upload a part of a multipart upload and had an bug that was hard to spot as minio told me "SUCCESS" for the upload itself..
I was trying to upload part of a multi-part upload and had an error that was hard to see because minio was showing me "SUCCESS" for the upload itself....

## Regression
I don't think so.

## Your Environment
* minio.exe version RELEASE.2023-05-18T00-05-36Z (commit-id=9d96b18df03f5978873705ac55959161f018fb48)
* Runtime: go1.19.9 windows/amd64

* Betriebsystemname	Microsoft Windows 10 Enterprise
* Version	10.0.19045 Build 19045

`minio.exe "server" "E:\temp\minio" "--address" ":9060" "--json" "--console-address" ":9001"`



Discussion / hints:
Must be a regression. I will investigate. Thanks for the detailed report.

Fix patch:
diff --git a/cmd/api-router.go b/cmd/api-router.go
index 3b77518888feb..76a3ddc2864d6 100644
--- a/cmd/api-router.go
+++ b/cmd/api-router.go
@@ -245,10 +245,10 @@ func registerAPIRouter(router *mux.Router) {
 		router.Methods(http.MethodPut).Path("/{object:.+}").
 			HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").
 			HandlerFunc(collectAPIStats("copyobjectpart", maxClients(gz(httpTraceAll(api.CopyObjectPartHandler))))).
-			Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
+			Queries("partNumber", "{partNumber:.*}", "uploadId", "{uploadId:.*}")
 		// PutObjectPart
 		router.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
-			collectAPIStats("putobjectpart", maxClients(gz(httpTraceHdrs(api.PutObjectPartHandler))))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
+			collectAPIStats("putobjectpart", maxClients(gz(httpTraceHdrs(api.PutObjectPartHandler))))).Queries("partNumber", "{partNumber:.*}", "uploadId", "{uploadId:.*}")
 		// ListObjectParts
 		router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
 			collectAPIStats("listobjectparts", maxClients(gz(httpTraceAll(api.ListObjectPartsHandler))))).Queries("uploadId", "{uploadId:.*}")
diff --git a/cmd/object-multipart-handlers.go b/cmd/object-multipart-handlers.go
index 6cf110cb1a004..7f9e366abe7cd 100644
--- a/cmd/object-multipart-handlers.go
+++ b/cmd/object-multipart-handlers.go
@@ -285,7 +285,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
 	partIDString := r.Form.Get(xhttp.PartNumber)
 
 	partID, err := strconv.Atoi(partIDString)
-	if err != nil {
+	if err != nil || partID <= 0 {
 		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidPart), r.URL)
 		return
 	}
@@ -615,7 +615,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
 	partIDString := r.Form.Get(xhttp.PartNumber)
 
 	partID, err := strconv.Atoi(partIDString)
-	if err != nil {
+	if err != nil || partID <= 0 {
 		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidPart), r.URL)
 		return
 	}


Test patch:
diff --git a/cmd/object-handlers_test.go b/cmd/object-handlers_test.go
index 97ce3574c459c..e603e4c251e45 100644
--- a/cmd/object-handlers_test.go
+++ b/cmd/object-handlers_test.go
@@ -3693,6 +3693,27 @@ func testAPIPutObjectPartHandler(obj ObjectLayer, instanceType, bucketName strin
 
 			expectedAPIError: ErrInvalidAccessKeyID,
 		},
+		// Case where part number is invalid.
+		9: {
+			objectName: testObject,
+			content:    "hello",
+			partNumber: "0",
+			fault:      None,
+			accessKey:  credentials.AccessKey,
+			secretKey:  credentials.SecretKey,
+
+			expectedAPIError: ErrInvalidPart,
+		},
+		10: {
+			objectName: testObject,
+			content:    "hello",
+			partNumber: "-10",
+			fault:      None,
+			accessKey:  credentials.AccessKey,
+			secretKey:  credentials.SecretKey,
+
+			expectedAPIError: ErrInvalidPart,
+		},
 	}
 
 	reqV2Str := "V2 Signed HTTP request"
diff --git a/cmd/test-utils_test.go b/cmd/test-utils_test.go
index c3ba383e855d0..c08843752d9eb 100644
--- a/cmd/test-utils_test.go
+++ b/cmd/test-utils_test.go
@@ -2008,10 +2008,10 @@ func registerBucketLevelFunc(bucket *mux.Router, api objectAPIHandlers, apiFunct
 			bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(api.NewMultipartUploadHandler).Queries("uploads", "")
 		case "CopyObjectPart":
 			// Register CopyObjectPart handler.
-			bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(api.CopyObjectPartHandler).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
+			bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(api.CopyObjectPartHandler).Queries("partNumber", "{partNumber:.*}", "uploadId", "{uploadId:.*}")
 		case "PutObjectPart":
 			// Register PutObjectPart handler.
-			bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(api.PutObjectPartHandler).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
+			bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(api.PutObjectPartHandler).Queries("partNumber", "{partNumber:.*}", "uploadId", "{uploadId:.*}")
 		case "ListObjectParts":
 			// Register ListObjectParts handler.
 			bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(api.ListObjectPartsHandler).Queries("uploadId", "{uploadId:.*}")