This is an automated email from the ASF dual-hosted git repository.

Gargi-jais11 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 1a9439452d9 HDDS-15809. Validate x-amz-copy-source-range on 
UploadPartCopy (#10711).
1a9439452d9 is described below

commit 1a9439452d9aae2ffd9ed4d20e8ac1f3a9f98864
Author: KUAN-HAO HUANG <[email protected]>
AuthorDate: Thu Jul 16 16:41:51 2026 +0800

    HDDS-15809. Validate x-amz-copy-source-range on UploadPartCopy (#10711).
---
 .../ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java   | 46 ++++++++++++++++++++++
 .../hadoop/ozone/s3/endpoint/ObjectEndpoint.java   | 19 +++++++--
 2 files changed, 62 insertions(+), 3 deletions(-)

diff --git 
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
 
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
index ecab86ed323..a3d396fc736 100644
--- 
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
+++ 
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
@@ -22,6 +22,7 @@
 import static org.apache.hadoop.ozone.s3.awssdk.S3SDKTestUtils.createFile;
 import static org.apache.hadoop.ozone.s3.util.S3Utils.stripQuotes;
 import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
+import static org.apache.http.HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -1414,6 +1415,51 @@ public void testCopyObjectWithDestinationIfMatchFail() {
     assertEquals("PreconditionFailed", 
exception.awsErrorDetails().errorCode());
   }
 
+  @Test
+  public void testUploadPartCopyInvalidRange() {
+    final String sourceBucketName = getBucketName("source");
+    final String destBucketName = getBucketName("dest");
+    final String sourceKey = getKeyName("source");
+    final String destKey = getKeyName("dest");
+    s3Client.createBucket(b -> b.bucket(sourceBucketName));
+    s3Client.createBucket(b -> b.bucket(destBucketName));
+
+    // Source object is exactly 5 bytes.
+    s3Client.putObject(b -> b.bucket(sourceBucketName).key(sourceKey), 
RequestBody.fromString("hello"));
+
+    CreateMultipartUploadResponse createResponse = 
s3Client.createMultipartUpload(b -> b
+        .bucket(destBucketName)
+        .key(destKey));
+    String uploadId = createResponse.uploadId();
+
+    UploadPartCopyRequest.Builder requestBuilder = 
UploadPartCopyRequest.builder()
+        .sourceBucket(sourceBucketName)
+        .sourceKey(sourceKey)
+        .destinationBucket(destBucketName)
+        .destinationKey(destKey)
+        .uploadId(uploadId)
+        .partNumber(1);
+
+    // Case 1: range beyond the source object length, and start > end -> 
InvalidRange.
+    // InvalidRange maps to HTTP 416; AWS also permits 400 for these cases.
+    for (String invalidRange : Arrays.asList("bytes=0-21", "bytes=3-1")) {
+      S3Exception outOfRange = assertThrows(S3Exception.class, () ->
+          
s3Client.uploadPartCopy(requestBuilder.copySourceRange(invalidRange).build()));
+      assertThat(outOfRange.statusCode())
+          .isIn(SC_BAD_REQUEST, SC_REQUESTED_RANGE_NOT_SATISFIABLE);
+      assertEquals("InvalidRange", outOfRange.awsErrorDetails().errorCode());
+    }
+
+    // Case 2: malformed range values -> InvalidArgument (mirrors s3-tests).
+    for (String malformedRange : Arrays.asList(
+        "0-2", "bytes=0", "bytes=hello-world", "bytes=0-bar", "bytes=hello-", 
"bytes=0-2,3-5")) {
+      S3Exception malformed = assertThrows(S3Exception.class, () ->
+          
s3Client.uploadPartCopy(requestBuilder.copySourceRange(malformedRange).build()));
+      assertThat(malformed.statusCode()).isEqualTo(SC_BAD_REQUEST);
+      assertEquals("InvalidArgument", malformed.awsErrorDetails().errorCode());
+    }
+  }
+
   @Test
   public void testLowLevelMultipartUpload(@TempDir Path tempDir) throws 
Exception {
     final String bucketName = getBucketName();
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
index 04e0765a9b2..f943042a0b7 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
@@ -38,6 +38,7 @@
 import static 
org.apache.hadoop.ozone.s3.util.S3Consts.DECODED_CONTENT_LENGTH_HEADER;
 import static org.apache.hadoop.ozone.s3.util.S3Consts.MP_PARTS_COUNT;
 import static org.apache.hadoop.ozone.s3.util.S3Consts.RANGE_HEADER;
+import static 
org.apache.hadoop.ozone.s3.util.S3Consts.RANGE_HEADER_MATCH_PATTERN;
 import static 
org.apache.hadoop.ozone.s3.util.S3Consts.RANGE_HEADER_SUPPORTED_UNIT;
 import static org.apache.hadoop.ozone.s3.util.S3Consts.STORAGE_CLASS_HEADER;
 import static org.apache.hadoop.ozone.s3.util.S3Consts.TAG_COUNT_HEADER;
@@ -58,6 +59,7 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Matcher;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
@@ -891,11 +893,22 @@ private Response createMultipartKey(OzoneVolume volume, 
OzoneBucket ozoneBucket,
             getHeaders().getHeaderString(COPY_SOURCE_HEADER_RANGE);
         RangeHeader rangeHeader = null;
         if (range != null) {
-          rangeHeader = RangeHeaderParserUtil.parseRangeHeader(range, 0);
+          Matcher matcher = RANGE_HEADER_MATCH_PATTERN.matcher(range);
+          if (!matcher.matches()
+              || matcher.group("start").isEmpty()
+              || matcher.group("end").isEmpty()) {
+            throw newError(S3ErrorTable.INVALID_ARGUMENT, range);
+          }
+          long startOffset = Long.parseLong(matcher.group("start"));
+          long endOffset = Long.parseLong(matcher.group("end"));
+          long sourceSize = sourceKeyDetails.getDataSize();
+          if (startOffset > endOffset || endOffset >= sourceSize) {
+            throw newError(S3ErrorTable.INVALID_RANGE, range);
+          }
+          rangeHeader = new RangeHeader(startOffset, endOffset, false, false);
           // When copy Range, the size of the target key is the
           // length specified by COPY_SOURCE_HEADER_RANGE.
-          length = rangeHeader.getEndOffset() -
-              rangeHeader.getStartOffset() + 1;
+          length = endOffset - startOffset + 1;
         } else {
           length = sourceKeyDetails.getDataSize();
         }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to