aws-sdk-ruby
aws-sdk-ruby copied to clipboard
service/s3: presigned PutObject with Tagging set does not add tagging to object in S3 bucket.
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
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.
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
.
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
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.
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.
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.
Soft update, S3 acknowledges that this is a bug and is working on a fix.