Repository: minio/minio (language: go)

Issue:
`X-Amz-Meta-Snowball-Auto-Extract` unexpected behavior with restrictive `key`
## Problem
I'm using the access key created with the policy below I should only be able to GET and PUT objects on bucket `tarbucket` and **only** when the prefix starts with `myuser/`. However, when using the automatic extraction feature provided by `X-Amz-Meta-Snowball-Auto-Extract=true`, the server seems to write the data into the bucket's "root", even if my access key doesn't allow me to do the analogous operations.

Side effect: This could have implications like data loss via `overwrite` on common clashing `keys`

## Replication
I generate an `accesskey` with the following policy (written in file `key-policy.json`) and add the respective alias `s3tar`
```json
{
 "Version": "2012-10-17",
 "Statement": [
  {
   "Effect": "Allow",
   "Action": [
    "s3:ListBucket"
   ],
   "Resource": [
    "arn:aws:s3:::tarbucket"
   ],
   "Condition": {
    "StringLike": {
     "s3:prefix": [
      "",
      "myuser/*"
     ]
    }
   }
  },
  {
   "Effect": "Allow",
   "Action": [
    "s3:DeleteObject",
    "s3:GetObject",
    "s3:PutObject"
   ],
   "Resource": [
    "arn:aws:s3:::tarbucket/myuser/*"
   ]
  }
 ]
}
```
```bash
mc admin accesskey create --policy key-policy.json --access-key tar-problem --secret-key tar-problem play/
mc alias set s3tar https://play.min.io tar-problem tar-problem

# recreate the bucket with `play` alias
mc rb --force play/tarbucket/
mc mb play/tarbucket/

# Create the only prefix this alias/accesskey should allow
mc mb s3tar/tarbucket/myuser
mc ls --recursive s3tar/tarbucket/

# This should FAIL!
echo helloworld! | mc pipe  s3tar/tarbucket/helloworld

#  Create a sample tar
mkdir -p mydir/{1..3}/{a..c}/
echo HelloWorld! | tee mydir/{1..3}/{a..c}/file.txt
tar -czvf  mydir.tar.gz mydir/

########################################################################
#  PROBLEM: This writes to a prefix which permissions should NOT allow #
########################################################################
mc cp  --disable-multipart --attr "X-Amz-Meta-Snowball-Auto-Extract=true" mydir.tar.gz s3tar/tarbucket/myuser/
# shows nothing
mc ls s3tar/tarbucket/myuser/
# Shows where the tar was extracted to, which is under `tarbucket/mydir/....`
mc ls --recursive s3tar/tarbucket/
# But listing the prefix directly show Access denied as expected
mc ls s3tar/tarbucket/mydir/
```
## Expected behaviour
I guess `Access Denied` should be the outcome of this. However, there's the possible fallback to:
```bash
# This works fine!
mc cp  --disable-multipart --attr "X-Amz-Meta-Snowball-Auto-Extract=true;X-Amz-Meta-Minio-Snowball-Prefix=myuser/" mydir.tar.gz s3tar/tarbucket/myuser/
```
I'm not sure about the role of the `prefix` in this call on "S3 standards", but it would make sense to me that it would fit well on the 'X-Amz-Meta-Minio-Snowball-Prefix' header?!

## Relevant Debug output
```
mc: <DEBUG> PUT /tarbucket/myuser/mydir.tar.gz HTTP/1.1
Host: play.min.io
User-Agent: MinIO (linux; amd64) minio-go/v7.0.83 mc/RELEASE.2025-01-17T23-25-50Z
Content-Length: 390
Accept-Encoding: zstd,gzip
Authorization: AWS4-HMAC-SHA256 Credential=tar-problem/20250130/us-east-1/s3/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-meta-snowball-auto-extract, Signature=**REDACTED**
Content-Type: application/gzip
X-Amz-Content-Sha256: UNSIGNED-PAYLOAD
X-Amz-Date: 20250130T164723Z
X-Amz-Meta-Snowball-Auto-Extract: true

mc: <DEBUG> HTTP/1.1 200 OK
Content-Length: 0
```
## Your Environment
* Server version (via 'mc admin info play'): `2024-12-21T04:24:45Z`
* Client version: `mc version RELEASE.2025-01-17T23-25-50Z (commit-id=cc8758c88da31b95dde7b0b6997980ebfb8b98c4)
Runtime: go1.23.5 linux/amd64
Copyright (c) 2015-2025 MinIO, Inc.
License GNU AGPLv3 <https://www.gnu.org/licenses/agpl-3.0.html>`
`X-Amz-Meta-Snowball-Auto-Extract` unexpected behavior with restrictive `key`
## Problem
I'm using the access key created with the policy below I should only be able to GET and PUT objects on bucket `tarbucket` and **only** when the prefix starts with `myuser/`. However, when using the automatic extraction feature provided by `X-Amz-Meta-Snowball-Auto-Extract=true`, the server seems to write the data into the bucket's "root", even if my access key doesn't allow me to do the analogous operations.

Side effect: This could have implications like data loss via `overwrite` on common clashing `keys`

## Replication
I generate an `accesskey` with the following policy (written in file `key-policy.json`) and add the respective alias `s3tar`
```json
{
 "Version": "2012-10-17",
 "Statement": [
  {
   "Effect": "Allow",
   "Action": [
    "s3:ListBucket"
   ],
   "Resource": [
    "arn:aws:s3:::tarbucket"
   ],
   "Condition": {
    "StringLike": {
     "s3:prefix": [
      "",
      "myuser/*"
     ]
    }
   }
  },
  {
   "Effect": "Allow",
   "Action": [
    "s3:DeleteObject",
    "s3:GetObject",
    "s3:PutObject"
   ],
   "Resource": [
    "arn:aws:s3:::tarbucket/myuser/*"
   ]
  }
 ]
}
```
```bash
mc admin accesskey create --policy key-policy.json --access-key tar-problem --secret-key tar-problem play/
mc alias set s3tar https://play.min.io tar-problem tar-problem

# recreate the bucket with `play` alias
mc rb --force play/tarbucket/
mc mb play/tarbucket/

# Create the only prefix this alias/accesskey should allow
mc mb s3tar/tarbucket/myuser
mc ls --recursive s3tar/tarbucket/

# This should FAIL!
echo helloworld! | mc pipe  s3tar/tarbucket/helloworld

#  Create a sample tar
mkdir -p mydir/{1..3}/{a..c}/
echo HelloWorld! | tee mydir/{1..3}/{a..c}/file.txt
tar -czvf  mydir.tar.gz mydir/

########################################################################
#  PROBLEM: This writes to a prefix which permissions should NOT allow #
########################################################################
mc cp  --disable-multipart --attr "X-Amz-Meta-Snowball-Auto-Extract=true" mydir.tar.gz s3tar/tarbucket/myuser/
# shows nothing
mc ls s3tar/tarbucket/myuser/
# Shows where the tar was extracted to, which is under `tarbucket/mydir/....`
mc ls --recursive s3tar/tarbucket/
# But listing the prefix directly show Access denied as expected
mc ls s3tar/tarbucket/mydir/
```
## Expected behaviour
I guess `Access Denied` should be the outcome of this. However, there's the possible fallback to:
```bash
# This works fine!
mc cp  --disable-multipart --attr "X-Amz-Meta-Snowball-Auto-Extract=true;X-Amz-Meta-Minio-Snowball-Prefix=myuser/" mydir.tar.gz s3tar/tarbucket/myuser/
```
I'm not sure about the role of the `prefix` in this call on "S3 standards", but it would make sense to me that it would fit well on the 'X-Amz-Meta-Minio-Snowball-Prefix' header?!

## Relevant Debug output
```
mc: <DEBUG> PUT /tarbucket/myuser/mydir.tar.gz HTTP/1.1
Host: play.min.io
User-Agent: MinIO (linux; amd64) minio-go/v7.0.83 mc/RELEASE.2025-01-17T23-25-50Z
Content-Length: 390
Accept-Encoding: zstd,gzip
Authorization: AWS4-HMAC-SHA256 Credential=tar-problem/20250130/us-east-1/s3/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-meta-snowball-auto-extract, Signature=**REDACTED**
Content-Type: application/gzip
X-Amz-Content-Sha256: UNSIGNED-PAYLOAD
X-Amz-Date: 20250130T164723Z
X-Amz-Meta-Snowball-Auto-Extract: true

mc: <DEBUG> HTTP/1.1 200 OK
Content-Length: 0
```
## Your Environment
* Server version (via 'mc admin info play'): `2024-12-21T04:24:45Z`
* Client version: `mc version RELEASE.2025-01-17T23-25-50Z (commit-id=cc8758c88da31b95dde7b0b6997980ebfb8b98c4)
Runtime: go1.23.5 linux/amd64
Copyright (c) 2015-2025 MinIO, Inc.
License GNU AGPLv3 <https://www.gnu.org/licenses/agpl-3.0.html>`


Discussion / hints:

Fix patch:
diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go
index b17a042a8cfd3..3c563b8cc6bd5 100644
--- a/cmd/object-handlers.go
+++ b/cmd/object-handlers.go
@@ -2231,8 +2231,15 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
 		putObject = objectAPI.PutObject
 	)
 
-	// Check if put is allowed
-	if s3Err = isPutActionAllowed(ctx, rAuthType, bucket, object, r, policy.PutObjectAction); s3Err != ErrNone {
+	var opts untarOptions
+	opts.ignoreDirs = strings.EqualFold(r.Header.Get(xhttp.MinIOSnowballIgnoreDirs), "true")
+	opts.ignoreErrs = strings.EqualFold(r.Header.Get(xhttp.MinIOSnowballIgnoreErrors), "true")
+	opts.prefixAll = r.Header.Get(xhttp.MinIOSnowballPrefix)
+	if opts.prefixAll != "" {
+		opts.prefixAll = trimLeadingSlash(pathJoin(opts.prefixAll, slashSeparator))
+	}
+	// Check if put is allow for specified prefix.
+	if s3Err = isPutActionAllowed(ctx, rAuthType, bucket, opts.prefixAll, r, policy.PutObjectAction); s3Err != ErrNone {
 		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
 		return
 	}
@@ -2300,7 +2307,10 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
 
 	putObjectTar := func(reader io.Reader, info os.FileInfo, object string) error {
 		size := info.Size()
-
+		if s3Err = isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.PutObjectAction); s3Err != ErrNone {
+			writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+			return errors.New(errorCodes.ToAPIErr(s3Err).Code)
+		}
 		metadata := map[string]string{
 			xhttp.AmzStorageClass: sc, // save same storage-class as incoming stream.
 		}
@@ -2336,7 +2346,7 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
 
 		if r.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String() {
 			if s3Err = isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.ReplicateObjectAction); s3Err != ErrNone {
-				return err
+				return errors.New(errorCodes.ToAPIErr(s3Err).Code)
 			}
 			metadata[ReservedMetadataPrefixLower+ReplicaStatus] = replication.Replica.String()
 			metadata[ReservedMetadataPrefixLower+ReplicaTimestamp] = UTCNow().Format(time.RFC3339Nano)
@@ -2483,14 +2493,6 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
 		return nil
 	}
 
-	var opts untarOptions
-	opts.ignoreDirs = strings.EqualFold(r.Header.Get(xhttp.MinIOSnowballIgnoreDirs), "true")
-	opts.ignoreErrs = strings.EqualFold(r.Header.Get(xhttp.MinIOSnowballIgnoreErrors), "true")
-	opts.prefixAll = r.Header.Get(xhttp.MinIOSnowballPrefix)
-	if opts.prefixAll != "" {
-		opts.prefixAll = trimLeadingSlash(pathJoin(opts.prefixAll, slashSeparator))
-	}
-
 	if err = untar(ctx, hreader, putObjectTar, opts); err != nil {
 		apiErr := errorCodes.ToAPIErr(s3Err)
 		// If not set, convert or use BadRequest


Test patch: