From c60393a1c4dec3cc778b9fca51a7fc16cc21cffa Mon Sep 17 00:00:00 2001 From: Eunbin Son Date: Fri, 25 Sep 2026 17:17:03 +0900 Subject: [PATCH 1/2] [oss] Fix blob presigned URL validation when fs.oss.sld.enabled is set With fs.oss.sld.enabled the shared OSS client signs path-style URLs (host = endpoint host, path = /bucket/key), but validatePresignedUrl always expected a virtual-hosted URL, so every presigned URL request was rejected as an invalid target. Expect the path-style shape when the client configuration has SLD enabled; the default path is unchanged and HTTPS is still required. Generated-by: Claude Code Co-Authored-By: Claude Opus 5.5 --- .../apache/paimon/oss/OSSBlobPresigner.java | 8 +- .../org/apache/paimon/oss/OSSFileIOTest.java | 104 ++++++++++++++++++ 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/paimon-filesystems/paimon-oss-impl/src/main/java/org/apache/paimon/oss/OSSBlobPresigner.java b/paimon-filesystems/paimon-oss-impl/src/main/java/org/apache/paimon/oss/OSSBlobPresigner.java index 55f9089e3217..c47e7de87533 100644 --- a/paimon-filesystems/paimon-oss-impl/src/main/java/org/apache/paimon/oss/OSSBlobPresigner.java +++ b/paimon-filesystems/paimon-oss-impl/src/main/java/org/apache/paimon/oss/OSSBlobPresigner.java @@ -219,11 +219,15 @@ private static String sha256Hex(byte[] bytes) { private static void validatePresignedUrl( OSSClient client, URL url, String bucket, String targetKey) throws Exception { URI endpoint = client.getEndpoint(); - String expectedHost = bucket + "." + endpoint.getHost(); + // With fs.oss.sld.enabled the client signs path-style URLs: the host is the endpoint and + // the bucket becomes the first path segment, so bucket and key are still both checked. + boolean pathStyle = client.getClientConfiguration().isSLDEnabled(); + String expectedHost = pathStyle ? endpoint.getHost() : bucket + "." + endpoint.getHost(); + String expectedPath = pathStyle ? "/" + bucket + "/" + targetKey : "/" + targetKey; if (!"https".equalsIgnoreCase(endpoint.getScheme()) || !"https".equalsIgnoreCase(url.getProtocol()) || !expectedHost.equalsIgnoreCase(url.getHost()) - || !("/" + targetKey).equals(url.toURI().getPath())) { + || !expectedPath.equals(url.toURI().getPath())) { throw new IOException("OSS client generated a presigned URL for an invalid target."); } } diff --git a/paimon-filesystems/paimon-oss-impl/src/test/java/org/apache/paimon/oss/OSSFileIOTest.java b/paimon-filesystems/paimon-oss-impl/src/test/java/org/apache/paimon/oss/OSSFileIOTest.java index e801d25f8156..629b14cdfc9a 100644 --- a/paimon-filesystems/paimon-oss-impl/src/test/java/org/apache/paimon/oss/OSSFileIOTest.java +++ b/paimon-filesystems/paimon-oss-impl/src/test/java/org/apache/paimon/oss/OSSFileIOTest.java @@ -93,6 +93,7 @@ public void testCreateBlobPresignedUrlMaterializesSinglePart() throws Exception copied.setPartNumber(1); copied.setETag("etag"); when(client.uploadPartCopy(any())).thenReturn(copied); + when(client.getClientConfiguration()).thenReturn(new ClientConfiguration()); when(client.getEndpoint()).thenReturn(URI.create("https://oss-cn-hangzhou.aliyuncs.com")); when(client.generatePresignedUrl(eq("bucket"), anyString(), any(), eq(HttpMethod.GET))) .thenAnswer( @@ -158,6 +159,7 @@ public void testCreateBlobPresignedUrlPreservesInternalEndpoint() throws Excepti new BlobDescriptor("oss://bucket/table/-internal.aliyuncs.com/source.blob", 0, 1); when(client.headObject(eq("bucket"), contains("_bloburl_"))) .thenReturn(matchingMetadata(descriptor)); + when(client.getClientConfiguration()).thenReturn(new ClientConfiguration()); when(client.getEndpoint()) .thenReturn(URI.create("https://oss-cn-hangzhou-internal.aliyuncs.com")); when(client.generatePresignedUrl(eq("bucket"), anyString(), any(), eq(HttpMethod.GET))) @@ -337,11 +339,112 @@ public void testCreateBlobPresignedUrlRejectsInvalidTarget() throws Exception { "https://bucket.oss-cn-hangzhou.aliyuncs.com/table/_bloburl_hash"); } + @Test + public void testCreateBlobPresignedUrlAcceptsPathStyleWhenSldEnabled() throws Exception { + BlobDescriptor descriptor = new BlobDescriptor("oss://bucket/table/source.blob", 0, 1); + String key = "table/_bloburl_" + sha256Hex(descriptor.serialize()); + for (String endpoint : + new String[] { + "https://oss-cn-hangzhou.aliyuncs.com", + "https://oss-cn-hangzhou-internal.aliyuncs.com", + "https://ep-abc.oss.cn-hangzhou.privatelink.aliyuncs.com" + }) { + String signed = endpoint + "/bucket/" + key + "?Signature=test"; + assertThat(presignWithSldMock(endpoint, descriptor, signed)).isEqualTo(signed); + } + + // The bucket is still verified, now as the first path segment. + assertThatThrownBy( + () -> + presignWithSldMock( + "https://oss-cn-hangzhou.aliyuncs.com", + descriptor, + "https://oss-cn-hangzhou.aliyuncs.com/other/" + key)) + .isInstanceOf(java.io.IOException.class) + .hasMessageContaining("invalid target"); + } + + @Test + public void testCreateBlobPresignedUrlAcceptsSdkPathStyleUrlWithEncodedKey() throws Exception { + BlobDescriptor descriptor = + new BlobDescriptor("oss://bucket/table/a b/\uD55C/source.blob", 0, 1); + + URL url = new URL(presignWithSldClient("https://oss-cn-hangzhou.aliyuncs.com", descriptor)); + + assertThat(url.getHost()).isEqualTo("oss-cn-hangzhou.aliyuncs.com"); + assertThat(url.toURI().getPath()) + .isEqualTo( + "/bucket/table/a b/\uD55C/_bloburl_" + sha256Hex(descriptor.serialize())); + } + + @Test + public void testCreateBlobPresignedUrlAcceptsSdkPathStyleUrlWithEndpointPort() + throws Exception { + BlobDescriptor descriptor = new BlobDescriptor("oss://bucket/table/source.blob", 0, 1); + + URL url = + new URL( + presignWithSldClient( + "https://oss-cn-hangzhou.aliyuncs.com:8443", descriptor)); + + assertThat(url.getHost()).isEqualTo("oss-cn-hangzhou.aliyuncs.com"); + assertThat(url.getPort()).isEqualTo(8443); + assertThat(url.getPath()) + .isEqualTo("/bucket/table/_bloburl_" + sha256Hex(descriptor.serialize())); + } + + private static String presignWithSldMock( + String endpoint, BlobDescriptor descriptor, String generatedUrl) throws Exception { + OSSClient client = mock(OSSClient.class); + when(client.headObject(eq("bucket"), contains("_bloburl_"))) + .thenReturn(matchingMetadata(descriptor)); + ClientConfiguration configuration = new ClientConfiguration(); + configuration.setSLDEnabled(true); + when(client.getClientConfiguration()).thenReturn(configuration); + when(client.getEndpoint()).thenReturn(URI.create(endpoint)); + when(client.generatePresignedUrl(eq("bucket"), anyString(), any(), eq(HttpMethod.GET))) + .thenReturn(presignedUrl(generatedUrl)); + return new TestOSSFileIO(client) + .createBlobPresignedUrl( + new Path("oss://bucket/table"), descriptor, Duration.ofMinutes(5)); + } + + /** + * Presigns with the URL the SDK itself signs once {@code fs.oss.sld.enabled} has turned on + * second-level domain mode, so the validator is checked against the real path-style output. + */ + private static String presignWithSldClient(String endpoint, BlobDescriptor descriptor) + throws Exception { + OSSClient signer = hadoopStyleClient(endpoint); + try { + signer.getClientConfiguration().setSLDEnabled(true); + OSSClient client = mock(OSSClient.class); + when(client.headObject(eq("bucket"), contains("_bloburl_"))) + .thenReturn(matchingMetadata(descriptor)); + when(client.getClientConfiguration()).thenReturn(signer.getClientConfiguration()); + when(client.getEndpoint()).thenReturn(signer.getEndpoint()); + when(client.generatePresignedUrl(eq("bucket"), anyString(), any(), eq(HttpMethod.GET))) + .thenAnswer( + invocation -> + signer.generatePresignedUrl( + "bucket", + invocation.getArgument(1), + invocation.getArgument(2), + HttpMethod.GET)); + return new TestOSSFileIO(client) + .createBlobPresignedUrl( + new Path("oss://bucket/table"), descriptor, Duration.ofMinutes(5)); + } finally { + signer.shutdown(); + } + } + private static void assertInvalidPresignedUrl(String endpoint, String generatedUrl) { OSSClient client = mock(OSSClient.class); BlobDescriptor descriptor = new BlobDescriptor("oss://bucket/table/source.blob", 0, 1); when(client.headObject(eq("bucket"), contains("_bloburl_"))) .thenReturn(matchingMetadata(descriptor)); + when(client.getClientConfiguration()).thenReturn(new ClientConfiguration()); when(client.getEndpoint()).thenReturn(URI.create(endpoint)); when(client.generatePresignedUrl(eq("bucket"), anyString(), any(), eq(HttpMethod.GET))) .thenReturn(presignedUrl(generatedUrl)); @@ -407,6 +510,7 @@ private static void stubMultipartUpload(OSSClient client) { } private static void stubPresigning(OSSClient client) { + when(client.getClientConfiguration()).thenReturn(new ClientConfiguration()); when(client.getEndpoint()).thenReturn(URI.create("https://oss-cn-hangzhou.aliyuncs.com")); when(client.generatePresignedUrl(eq("bucket"), anyString(), any(), eq(HttpMethod.GET))) .thenAnswer( From e4fb0e11c697a852ba6cd59457d49df6cd5a4912 Mon Sep 17 00:00:00 2001 From: Eunbin Son Date: Fri, 2 Oct 2026 13:21:45 +0900 Subject: [PATCH 2/2] [oss] Stub client configuration in the Jindo presigner test OSSBlobPresigner.validatePresignedUrl now reads getClientConfiguration().isSLDEnabled(). JindoFileIOTest exercises the same presigner through JindoBlobPresigner, and its Mockito OSSClient left getClientConfiguration() unstubbed, so the call hit a null and failed testCreateBlobPresignedUrlUsesOssClient. Stub it with a default ClientConfiguration, as the OSS fixtures already do. Generated-by: Claude Code Co-Authored-By: Claude Opus 5.5 --- .../src/test/java/org/apache/paimon/jindo/JindoFileIOTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/paimon-filesystems/paimon-jindo/src/test/java/org/apache/paimon/jindo/JindoFileIOTest.java b/paimon-filesystems/paimon-jindo/src/test/java/org/apache/paimon/jindo/JindoFileIOTest.java index 06b430a2a23f..17ee5e6d5bfe 100644 --- a/paimon-filesystems/paimon-jindo/src/test/java/org/apache/paimon/jindo/JindoFileIOTest.java +++ b/paimon-filesystems/paimon-jindo/src/test/java/org/apache/paimon/jindo/JindoFileIOTest.java @@ -27,6 +27,7 @@ import org.apache.paimon.utils.Pair; import com.aliyun.jindodata.common.JindoHadoopSystem; +import com.aliyun.oss.ClientConfiguration; import com.aliyun.oss.HttpMethod; import com.aliyun.oss.OSSClient; import com.aliyun.oss.model.ObjectMetadata; @@ -124,6 +125,7 @@ public void testCreateBlobPresignedUrlUsesOssClient() throws Exception { "paimon-blob-descriptor-sha256", sha256Hex(descriptor.serialize())); when(client.headObject(eq("bucket"), contains("_bloburl_"))).thenReturn(metadata); when(client.getEndpoint()).thenReturn(URI.create("https://oss.example.com")); + when(client.getClientConfiguration()).thenReturn(new ClientConfiguration()); when(client.generatePresignedUrl(eq("bucket"), anyString(), any(), eq(HttpMethod.GET))) .thenAnswer( invocation ->