aws-sdk-ruby icon indicating copy to clipboard operation
aws-sdk-ruby copied to clipboard

service/s3: presigned PutObject with Tagging set does not add tagging to object in S3 bucket.

Open ivanphdz opened this issue 1 year ago • 7 comments

Describe the bug

Objects uploaded to S3 with presigned Put Object URL with Tagging parameter set, do not have the tagging metadata. The SDK hoists the x-amz-tagging header to the query string, which is ignored by S3. S3 requires the x-amz-tagging to be a signed header.

Expected Behavior

x-amz-tagging must be a signed header

http://localhost:4566/sample-bucket/my-file3.txt?x-amz-server-side-encryption=AES256&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=test%2F20221005%2Fuus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221004T125303Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host%3Bx-amz-tagging&X-Amz-Signature=f5d10a2bdf42fc460ece2f070f9269c21ba301de3c01af59c52dcac27a12sq23

Current Behavior

Example

def get_presigned_url(bucket, object_key)
  url = bucket.object(object_key).presigned_url(:put, expires_in: 300, server_side_encryption: "AES256", tagging: "key1=value1")
  puts "Created presigned URL: #{url}."
  URI(url)
rescue Aws::Errors::ServiceError => e
  puts "Couldn't create presigned URL for #{bucket.name}:#{object_key}. Here's why: #{e.message}"
end

Result

http://localhost:4566/sample-bucket/my-file3.txt?x-amz-server-side-encryption=AES256&x-amz-tagging=key1%3Dvalue1&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=test%2F20221005%2Fuus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221004T125303Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=f5d10a2bdf42fc460ece2f070f9269c21ba301de3c01af59c52dcac27a12sq23

Reproduction Steps

require "aws-sdk-s3"
require "net/http"
require 'uri'

#"x-amz-tagging" => "Key2=Value2&Key1=Value1"

def get_presigned_url(bucket, object_key)
  url = bucket.object(object_key).presigned_url(:put, expires_in: 300, server_side_encryption: "AES256", tagging: "key1=value1")
  puts "Created presigned URL: #{url}."
  URI(url)
rescue Aws::Errors::ServiceError => e
  puts "Couldn't create presigned URL for #{bucket.name}:#{object_key}. Here's why: #{e.message}"
end

def run_demo
  bucket_name = "sample-bucket"
  object_key = "my-file3.txt"
  object_content = "This is the content of my-file.txt."

  bucket = Aws::S3::Bucket.new(bucket_name)
  presigned_url = get_presigned_url(bucket, object_key)
  return unless presigned_url
  response = Net::HTTP.start(presigned_url.host) do |http|
    http.send_request("PUT", presigned_url.request_uri, object_content, "content_type" => "")
  end

  case response
  when Net::HTTPSuccess
    puts "Content uploaded!"
  else
    puts response.value
  end
end

run_demo ```

### Possible Solution

_No response_

### Additional Information/Context

_No response_

### Gem name ('aws-sdk', 'aws-sdk-resources' or service gems like 'aws-sdk-s3') and its version

aws-sdk-s3

### Environment details (Version of Ruby, OS environment)

3.1.2

ivanphdz avatar Oct 04 '22 13:10 ivanphdz

Thanks for opening an issue. I agree that the tagging for pre-signed URL is not working correctly. Were you able to get this case to succeed? I made some local code changes to the SDK that has the tagging header on the query string (&x-amz-tagging=key1%3Dvalue1) AND also a signed header (X-Amz-SignedHeaders=host%3Bx-amz-server-side-encryption%3Bx-amz-tagging), and the request succeeds, but I don't see the tag on the object in the console! I tried without hoisting the header, but also signing it, as you suggested, and that returns a 403, likely signature mismatch.

mullermp avatar Oct 04 '22 15:10 mullermp

A url with x-amz-server-side-encryption hoisted but not signed seems to work as expected, the object is encrypted, and when not present, it is not. So I don't think the requirement is that the header must be signed. It might be that S3 just shouldn't be ignoring x-amz-tagging.

mullermp avatar Oct 04 '22 15:10 mullermp

I'll follow up with S3 on this. A work around is that you can provide the header to your Net::HTTP request and it succeeds. Instead of presigned_url, you can use presigned_request which returns a tuple of URL and headers to send.

  url, headers = bucket.object(object_key).presigned_request(:put, expires_in: 300, server_side_encryption: "AES256", tagging: "Key1=Value1")
  puts "Created presigned URL: #{url}."
  [URI(url), headers]


  presigned_url, headers = get_presigned_url(bucket, object_key)
  return unless presigned_url
  response = Net::HTTP.start(presigned_url.host) do |http|
    http.send_request("PUT", presigned_url.request_uri, object_content, headers)
  end

mullermp avatar Oct 04 '22 16:10 mullermp

yeah S3 ignores the x-amz-tagging query param, you need to provide it as a header to get the objects tagged correctly, I realized that because I test the same scenario in golang, and the Go-lang SDK is adding the x-amz-tagging values in the Signature calculation header and you need to provide the x-amz-tagging with the same values as a header to work.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/credentials"
	"github.com/aws/aws-sdk-go-v2/service/s3"
)

func main() {

	client := s3.New(options)
	name := "bucketname"
	fmt.Println("Upload an object to the bucket")

	fmt.Println("Create Presign client")
	presignClient := s3.NewPresignClient(client)
	presignParams := &s3.PutObjectInput{
		Bucket:  aws.String(name),
		Key:     aws.String("myfilego.txt"),
		Tagging: aws.String("Key1=Value2"),
	}

	// Apply an expiration via an option function
	presignDuration := func(po *s3.PresignOptions) {
		po.Expires = 5 * time.Minute
	}

	presignResult, err := presignClient.PresignPutObject(context.TODO(), presignParams, presignDuration)

	if err != nil {
		panic("Couldn't get presigned URL for PutObject")
	}

	fmt.Printf("Presigned URL For object: %s\n", presignResult.URL)
}

Result

curl --request PUT 'https://bucketname.s3.amazonaws.com/myfilego.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA3XE3RPJZRSVRC44W%2F2022100ew%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20221004T165857Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host%3Bx-amz-tagging&x-id=PutObject&X-Amz-Signature=293e5d1a6504f98846fc51ebe709e21f7579ee58bb5cdf418c0b2adc7514dffdawe'
--header 'x-amz-tagging: Key1=Value2'

(I added the header manually, but this works as expected)

I agree it's really confusing anyway, thanks I will test the workaround.

ivanphdz avatar Oct 04 '22 17:10 ivanphdz

Yes. That's why we have both presigned_url and presigned_request. In theory the URL with hoisted query params should be working! It works with SSE. Calling presigned_request will not hoist but it will return you the header (x-amz-tagging) that you need to include with your request. I do think this is a bug on S3's side. The chances are slim that it will be fixed but I'll get in contact with them.

mullermp avatar Oct 04 '22 17:10 mullermp

We've created an internal tracking ticket with S3 regarding this. In the mean time, I would rely on sending the header directly instead of hoisting it. The presigned_request method should work for your case.

mullermp avatar Oct 10 '22 15:10 mullermp

Soft update, S3 acknowledges that this is a bug and is working on a fix.

mullermp avatar Nov 28 '22 16:11 mullermp