[ 
https://issues.apache.org/jira/browse/HDDS-17630?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18135628#comment-18135628
 ] 

Ritesh Shukla commented on HDDS-17630:
--------------------------------------

Updated the description after a second test against AWS S3 on 2026-10-31. S3 
answers 405 MethodNotAllowed to a PUT with any {{uploadId}} and no 
{{partNumber}}, not only an empty one, so the proposed fix now covers both. It 
refuses any {{partNumber}} value ({{0}}, {{abc}}, empty) without an upload ID 
with the same 400, also while an upload is in progress on the key. {{HEAD 
?uploadId=}} and {{GET ?partNumber=1&uploadId=}} get 405 on S3; that difference 
is noted and left out of this fix.

----
🤖 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 in the S3 Gateway 
> unit-test harness on master:
> || 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 | same as above: the gateway's JAX-RS layer decodes a bare {{uploadId}} 
> to the same empty value (checked with Jersey's query decoder) |
> | {{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 | unknown ID: 404 
> NoSuchUpload; real upload: OM refuses part number 0 with 400 (read from code) 
> |
> 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.
> Other handlers that read {{uploadId}}:
> * {{POST ?uploadId=}} (CompleteMultipartUpload) and {{GET ?uploadId=}} 
> (ListParts): S3 and the gateway both answer 404 NoSuchUpload. On the gateway 
> side this was run against the test client; that OM refuses an empty upload ID 
> was read from code.
> * {{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 (run), and with it the object's metadata 
> and tags (read from code). 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 (inferred).
> * 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:214 on 
> master). Every other request becomes a PutObject, or a CopyObject when 
> {{x-amz-copy-source}} is set (ObjectEndpoint.java:236-243). That path never 
> reads {{partNumber}}.
> 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}}. A refused request is 
> audited and counted as a failed PutObject, and the key is never opened. 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. OM would answer 404 NoSuchUpload, not S3's 400. {{PUT 
> ?partNumber=1}} with no {{uploadId}} would still overwrite the object. And 
> UploadPartCopy would read the source key before failing. 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}}: one parameterized case for each of 
> {{?partNumber=1&uploadId=}}, {{?partNumber=1}} and {{?uploadId=}}. Each puts 
> a key, sends the PUT, checks S3's error code and message, and checks that the 
> key's content and size are unchanged. It runs with and without data stream. 
> On master all six runs fail: the PUT succeeds and the key is replaced. To add 
> with the fix: {{?partNumber=}}, {{?partNumber=0}} and {{?partNumber=abc}} 
> without an upload ID (400), and {{?uploadId=<non-empty>}} without a part 
> number (405).
> * Unit, {{TestMultipartUploadWithCopy}}: the same for UploadPartCopy with 
> {{?partNumber=1&uploadId=}}. On master the destination is replaced by the 
> source.
> * {{EndpointTestUtils#put}} stops adding {{partNumber=0}} to plain PUTs. A 
> real PutObject carries no {{partNumber}}, and the new check would refuse it.
> * All s3gateway unit tests pass with the change.
> * Integration (to add with the fix): a case in {{AbstractS3SDKV2Tests}} calls 
> {{uploadPart}} and {{uploadPartCopy}} with {{uploadId("")}}, which the SDK 
> sends as {{uploadId=}}. It expects 400 InvalidArgument and the same ETag from 
> {{headObject}} afterwards.
> ----
> 🤖 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]

Reply via email to