[
https://issues.apache.org/jira/browse/HDDS-17630?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18135895#comment-18135895
]
Ritesh Shukla commented on HDDS-17630:
--------------------------------------
Corrected the description after re-checking it against master at ef63bacef7,
this time also on a mini cluster with the AWS SDK for Java v2. A PUT with a
non-empty {{uploadId}} and no {{partNumber}} is already refused on a real
cluster, with 400 InvalidArgument from the part-number check in Ozone's client
library; the 404 in the earlier table came from the unit tests' client stub.
Since HDDS-17498, the server-side encryption check runs before the gateway
picks the operation, so a malformed PUT that also asks for encryption the
gateway cannot honour now fails with 501 and keeps the object; the cited line
numbers also moved. The Testing section now describes the tests as written, and
the lost metadata and tags, the last-part-wins case and the POST and GET
answers on OM were run rather than read or inferred.
----
🤖 Generated with [Claude Code|https://claude.com/claude-code]
> S3G: a part upload with an empty or missing uploadId overwrites the object
> instead of failing
> ---------------------------------------------------------------------------------------------
>
> Key: HDDS-17630
> URL: https://issues.apache.org/jira/browse/HDDS-17630
> Project: Apache Ozone
> Issue Type: Bug
> Components: S3
> Reporter: Ritesh Shukla
> Assignee: Ritesh Shukla
> Priority: Major
>
> h3. Problem
> An S3 {{PUT}} on an object key means one of two things:
> * {{PUT /bucket/key?partNumber=N&uploadId=<id>}} uploads one part of a
> multipart upload (UploadPart). Nothing under the key changes until the upload
> is completed.
> * {{PUT /bucket/key}} writes the object (PutObject).
> When {{uploadId}} is empty, or {{partNumber}} comes with no {{uploadId}} at
> all, the S3 Gateway handles the request as a PutObject, or as a CopyObject
> when {{x-amz-copy-source}} is set. The body (or the copy source) replaces the
> committed object, and the client gets {{200 OK}}, just as for a part upload:
> {code}
> PUT /bucket/key body "original object" -> 200, key
> holds "original object"
> PUT /bucket/key?partNumber=1&uploadId= body "part 1" -> 200, key
> now holds "part 1"
> {code}
> AWS S3 refuses each of these requests and leaves the object alone (tested
> against AWS S3 on 2026-10-31). The Ozone column was run on master
> (ef63bacef7) against a mini cluster with the AWS SDK for Java v2, and in the
> S3 Gateway unit-test harness:
> || Request || AWS S3 || S3 Gateway on master ||
> | {{PUT ?partNumber=1&uploadId=}} | 400 InvalidArgument, object kept | 200,
> object replaced by the body |
> | {{PUT ?partNumber=1&uploadId}} (no {{=}}) | 400 InvalidArgument, object
> kept | 200, object replaced by the body: the gateway reads a bare
> {{uploadId}} as the same empty value |
> | {{PUT ?partNumber=1}} (no {{uploadId}}) | 400 InvalidArgument, object kept
> | 200, object replaced by the body |
> | {{PUT ?partNumber=1&uploadId=}} with {{x-amz-copy-source}} (UploadPartCopy)
> | 400 InvalidArgument, object kept | 200, object replaced by a full copy of
> the source; {{x-amz-copy-source-range}} is ignored |
> | {{PUT ?uploadId=}} (no {{partNumber}}) | 405 MethodNotAllowed, object kept
> | 200, object replaced by the body |
> | {{PUT ?partNumber=}} or {{?partNumber=0}} (no {{uploadId}}), or
> {{?partNumber=&uploadId=}} | 400 InvalidArgument, object kept
> ({{?partNumber=abc}} too) | 200, object replaced by the body |
> | {{PUT ?partNumber=1&uploadId=}} while an upload is in progress on the key |
> 400 InvalidArgument, object kept, no part added | 200, object replaced by the
> body |
> | {{PUT ?uploadId=<non-empty ID>}} (no {{partNumber}}), unknown or real
> upload | 405 MethodNotAllowed, object kept, no part added | 400
> InvalidArgument "Part number must be an integer between 1 and 10000,
> inclusive", object kept, no part added: the gateway reads the missing part
> number as 0, and Ozone's client library in the gateway refuses it before
> calling OM |
> S3's InvalidArgument message is "This operation does not accept partNumber
> without uploadId", for any value of {{partNumber}}. With an empty body, the
> gateway truncates the object to 0 bytes (run).
> On master, the server-side encryption check added by HDDS-17498 runs before
> the gateway picks the operation. So a request that also asks for encryption
> the gateway cannot honour (SSE-C in any bucket, or SSE-S3 or SSE-KMS on an
> unencrypted bucket) is refused first with 501 NotImplemented and keeps the
> object (run for SSE-C and SSE-S3 on an unencrypted bucket, the other cases
> read from code). Without an encryption header, or with one an encrypted
> bucket can honour, the object is replaced as above (run).
> Other handlers that read {{uploadId}} (gateway answers run against a mini
> cluster):
> * {{POST ?uploadId=}} (CompleteMultipartUpload) and {{GET ?uploadId=}}
> (ListParts): S3 and the gateway both answer 404 NoSuchUpload.
> * {{DELETE ?uploadId=}}: S3 answers 404 NoSuchUpload; the gateway deletes the
> object. That is HDDS-17599.
> * {{HEAD ?uploadId=}} and {{GET ?partNumber=1&uploadId=}}: S3 answers 405
> MethodNotAllowed; the gateway answers 200 (HeadObject) and 404 NoSuchUpload.
> Both are read-only and are left out of this fix.
> h3. Impact
> * Only a caller who may already write the key can do this. The operation that
> actually runs is authorized as itself: PutObject, or GetObject on the source
> and PutObject on the destination for the copy form. No permission boundary is
> crossed (read from code).
> * What is lost: the object's data, metadata and tags (run). The response is a
> success, so the client cannot tell. A client that uploads several parts under
> the same empty ID ends up with only the last part as the object (run).
> * How a client gets here: an application bug that passes an empty upload ID,
> for example after losing the CreateMultipartUpload response. Stock clients
> send it as-is. The AWS SDK for Java v2 (2.55.6) and v1 (1.12.788), and the
> AWS CLI v2 (2.35.10), all put {{uploadId=}} on the wire for an empty ID,
> including for UploadPartCopy; both SDKs refuse a null ID before sending (run
> against a local capture server).
> h3. Cause
> {{ObjectEndpoint#handlePutRequest}} treats a request as a part upload only
> when {{uploadID != null && !uploadID.equals("")}} (ObjectEndpoint.java:216 at
> ef63bacef7). Every other request becomes a PutObject, or a CopyObject when
> {{x-amz-copy-source}} is set (ObjectEndpoint.java:238-245). That path never
> reads {{partNumber}}. The only checks that run before this condition are the
> bucket lookup and the server-side encryption check
> (ObjectEndpoint.java:210-212).
> The empty-string test dates from the first UploadPart code (HDDS-902). The
> parameter was then declared with {{@DefaultValue("")}}, so a missing
> {{uploadId}} arrived as an empty string, and comparing with {{""}} was how
> "absent" was written. HDDS-14209 switched to reading query parameters
> directly, where a missing {{uploadId}} is {{null}}, but kept the comparison.
> So a present but empty {{uploadId}} still counts as absent.
> h3. Proposed fix
> Answer as S3 does (tested against AWS S3 on 2026-10-31), before any OM call:
> * {{partNumber}} present and {{uploadId}} missing or empty: 400
> {{InvalidArgument}} with S3's message, "This operation does not accept
> partNumber without uploadId". This also covers UploadPartCopy.
> * {{uploadId}} present, empty or not, and no {{partNumber}}: 405
> {{MethodNotAllowed}}, as S3 answers both. {{S3ErrorTable}} gains a
> {{METHOD_NOT_ALLOWED}} entry with S3's code and message.
> * Everything else is unchanged: a non-empty {{uploadId}} with a
> {{partNumber}} is still a part upload, and a PUT with neither parameter is
> still a PutObject.
> Both checks go at the top of {{handlePutRequest}}, before the bucket lookup
> and the server-side encryption check, so a refused request makes no OM call
> and the key is never opened. It is audited and counted as a failed PutObject.
> The part-upload branch then only needs {{uploadID != null}}.
> The obvious one-line fix, dropping {{!uploadID.equals("")}} so the empty ID
> reaches OM, is worse. Run against a mini cluster, it answers the empty ID
> with OM's 404 NoSuchUpload, not S3's 400, and {{PUT ?partNumber=1}} with no
> {{uploadId}} still overwrites the object. UploadPartCopy would also read the
> source key before failing (read from code). HDDS-17599 answers DELETE with
> NoSuchUpload because that is what S3 does for DELETE; S3 answers PUT
> differently, so this fix does too.
> h3. Testing
> * Unit, {{TestPartUpload}}: a PUT with {{partNumber}} and no non-empty
> {{uploadId}} ({{?partNumber=1&uploadId=}}, {{?partNumber=1}},
> {{?partNumber=}}, {{?partNumber=0}}, {{?partNumber=abc}},
> {{?partNumber=&uploadId=}}) expects S3's 400; a PUT with {{uploadId}} and no
> {{partNumber}} ({{?uploadId=}}, {{?uploadId=<unknown>}}) expects S3's 405;
> and with an upload in progress on the key, {{?partNumber=1&uploadId=}} (400)
> and {{?uploadId=<that upload>}} (405) must add no part. Each test puts a key
> first and checks that its content is unchanged. They run with and without
> data stream. On master all 20 runs fail: 16 replace the key, 2 answer 404
> instead of 405, and 2 are accepted by the unit tests' client stub, which does
> not check part numbers.
> * Unit, {{TestMultipartUploadWithCopy}}: UploadPartCopy with
> {{?partNumber=1&uploadId=}} and with {{?partNumber=1}}. On master both copy
> the source over the destination.
> * {{EndpointTestUtils#put}} stops adding {{partNumber=0}} to plain PUTs and
> clears a left-over {{uploadId}}. A real PutObject carries neither, and the
> new checks would refuse it: with the fix but without this change, 275
> existing unit-test runs in 17 classes fail.
> * Integration, {{AbstractS3SDKV2Tests}}: {{uploadPart}} and
> {{uploadPartCopy}} with {{uploadId("")}}, which the SDK sends as
> {{uploadId=}}. Each expects 400 InvalidArgument with S3's message and the
> object's content unchanged. On master the object is replaced.
> * Run on ef63bacef7 with the change: all 1,943 s3gateway unit-test runs pass,
> and so do the 255 tests of the SDK v1 and v2 integration suite
> ({{TestS3SDK}}). Without the change, only the new tests fail.
> ----
> 🤖 Generated with [Claude Code|https://claude.com/claude-code]
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]