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]