Skip to content

CopyObject returns 403 on Requester Pays buckets: the source-size HEAD drops x-amz-request-payer #652

Description

@ph-tgs

Describe the bug

A client configured for Requester Pays (i.e. sending x-amz-request-payer: requester) succeeds on GetObject, PutObject, and HeadObject against a Requester Pays bucket, but its CopyObject meta-request fails with 403 Forbidden — even for a same-bucket copy where the caller holds both s3:GetObject and s3:PutObject.

Root cause

Every CopyObject meta-request first issues a HEAD on the source object to learn its size, before any small-vs-multipart branching (source/s3_copy_object.c, AWS_S3_COPY_OBJECT_REQUEST_TAG_GET_OBJECT_SIZE). That HEAD is built in aws_s3_get_source_object_size_message_new() (source/s3_request_messages.c) from a fresh aws_http_message that inherits none of the original request's headers — so x-amz-request-payer is never set on it. On a Requester Pays bucket the un-headered HEAD is rejected with 403, failing the whole copy before any other sub-request runs.

This is unique to the size-HEAD. The other copy sub-requests forward headers and are unaffected:

  • Small-object BYPASS (CopyObject) uses aws_s3_message_util_copy_http_message_no_body_all_headers → copies all headers.
  • CreateMultipartUpload / UploadPartCopy / CompleteMultipartUpload / AbortMultipartUpload use exclusion-list copies that do not exclude x-amz-request-payer.

It is also why plain GetObject / PutObject / HeadObject (and the auto-ranged GET/PUT paths) work — they copy all headers from the template message.

Reproduction

  1. Cross-account: bucket in account A with Requester Pays enabled; caller in account B granted s3:GetObject + s3:PutObject via bucket policy.
  2. Configure the client to send x-amz-request-payer: requester.
  3. GetObject / PutObject / HeadObjectOK. CopyObject (e.g. a same-bucket rename) → 403.

Proposed fix

In aws_s3_get_source_object_size_message_new(), forward the request-scoped headers from the original request onto the source-size HEAD, in both the copy_source_uri branch and the x-amz-copy-source fallback branch:

  • x-amz-request-payer → forwarded verbatim.
  • x-amz-source-expected-bucket-owner → mapped to x-amz-expected-bucket-owner on the HEAD. This is the header that asserts the source owner on a CopyObject; the plain x-amz-expected-bucket-owner asserts the destination owner and must not be forwarded onto a read of the source object, or legitimate cross-account copies would newly 403.

Motivating context

Surfaced by awslabs/s3-connector-for-pytorch#435 (adds a requester_pays client option; closes awslabs/s3-connector-for-pytorch#430). With that option enabled, GET/PUT/HEAD work but S3FileSystem.rename (implemented as copy + delete) 403s on Requester Pays buckets. The connector reaches aws-c-s3 through mountpoint-s3 → mountpoint-s3-crt-sys, so the fix belongs here.

Environment

  • aws-c-s3: main (reproduced at commit 448ec5e)
  • Also affects the version vendored by mountpoint-s3-crt-sys 0.13.1.

A fix plus regression test is ready; PR to follow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions