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

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 2199def5 [ISSUE #1509] Block cloud metadata endpoints missed by Java 
address checks (#1510)
2199def5 is described below

commit 2199def5905de7771b9ca0ceb3f7e91b1194e5a9
Author: youngkermit8-coder <[email protected]>
AuthorDate: Tue Aug 11 20:19:40 2026 +0800

    [ISSUE #1509] Block cloud metadata endpoints missed by Java address checks 
(#1510)
    
    * [ISSUE #1509] Block the AWS IMDS IPv6 endpoint
    
    Signed-off-by: youngkermit8-coder <[email protected]>
    
    * fix(security): block Alibaba Cloud metadata endpoint
    
    Signed-off-by: youngkermit8-coder <[email protected]>
    
    * test(security): cover IPv4-mapped metadata address
    
    Signed-off-by: youngkermit8-coder <[email protected]>
    
    ---------
    
    Signed-off-by: youngkermit8-coder <[email protected]>
---
 .../rocketmq/studio/settings/SettingsService.java  | 28 +++++++++++----
 .../studio/settings/SettingsServiceTest.java       | 40 ++++++++++++++++++++++
 2 files changed, 62 insertions(+), 6 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java 
b/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
index ba1744e0..80873331 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
@@ -39,6 +39,7 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.UnknownHostException;
 import java.time.Duration;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
@@ -48,6 +49,14 @@ import java.util.UUID;
 @Service
 public class SettingsService {
 
+    private static final List<byte[]> CLOUD_METADATA_ADDRESSES = List.of(
+            new byte[] {
+                (byte) 0xfd, 0x00, 0x0e, (byte) 0xc2,
+                0x00, 0x00, 0x00, 0x00,
+                0x00, 0x00, 0x00, 0x00,
+                0x00, 0x00, 0x02, 0x54
+            }, // AWS IMDS IPv6: fd00:ec2::254
+            new byte[] {100, 100, 100, (byte) 200}); // Alibaba Cloud ECS 
metadata
     private static final Set<String> PROMETHEUS_COMPATIBLE_TYPES = Set.of(
             "prometheus", "victoriametrics", "thanos", "mimir", "cortex", 
"arms");
     private static final String PROMETHEUS_TEST_QUERY = "up";
@@ -251,11 +260,12 @@ public class SettingsService {
 
     /**
      * SSRF guard: the test endpoint performs a server-side HTTP request to an 
attacker-supplied
-     * URL. The hostname {@code localhost}, loopback IPs (127.x.x.x, ::1) and 
link-local addresses
-     * (169.254.x.x, fe80:: — the cloud metadata range) are never legitimate 
Prometheus endpoints
-     * and are rejected. Private site-local ranges stay allowed because 
on-premise Prometheus
-     * servers live on the internal network and the endpoint itself requires 
admin rights.
-     * Package-private so tests can admit the loopback-bound embedded test 
server.
+     * URL. The hostname {@code localhost}, loopback IPs (127.x.x.x, ::1), 
link-local addresses
+     * (169.254.x.x, fe80:: — the cloud metadata range), and known metadata 
endpoints not covered
+     * by Java's address categories are never legitimate Prometheus endpoints 
and are rejected.
+     * Private site-local ranges stay allowed because on-premise Prometheus 
servers live on the
+     * internal network and the endpoint itself requires admin rights. 
Package-private so tests
+     * can admit the loopback-bound embedded test server.
      */
     boolean isAllowedDataSourceHost(String host) {
         if (!StringUtils.hasText(host)) {
@@ -269,13 +279,19 @@ public class SettingsService {
             InetAddress address = InetAddress.getByName(normalized);
             return !address.isAnyLocalAddress()
                     && !address.isLinkLocalAddress()
-                    && !address.isLoopbackAddress();
+                    && !address.isLoopbackAddress()
+                    && !isKnownCloudMetadataAddress(address);
         } catch (UnknownHostException exception) {
             // Unresolvable host: let the connection attempt surface the real 
connectivity error.
             return true;
         }
     }
 
+    private boolean isKnownCloudMetadataAddress(InetAddress address) {
+        return CLOUD_METADATA_ADDRESSES.stream()
+                .anyMatch(metadataAddress -> 
Arrays.equals(address.getAddress(), metadataAddress));
+    }
+
     private DataSourceTestResultVO prometheusSuccess(JsonNode response) {
         if (response != null && 
"success".equals(response.path("status").asText())) {
             return DataSourceTestResultVO.builder()
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
index 0b264c72..ee111fcf 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
@@ -460,6 +460,46 @@ class SettingsServiceTest {
         assertThat(result.getMessage()).contains("local or private address");
     }
 
+    @Test
+    void testConnectionShouldRejectAwsImdsIpv6Address() {
+        DataSourceTestDTO compressedRequest = DataSourceTestDTO.builder()
+                .url("http://[fd00:ec2::254]/latest/meta-data/";)
+                .type("Prometheus")
+                .build();
+        DataSourceTestDTO expandedRequest = DataSourceTestDTO.builder()
+                
.url("http://[fd00:0ec2:0000:0000:0000:0000:0000:0254]/latest/meta-data/";)
+                .type("Prometheus")
+                .build();
+
+        DataSourceTestResultVO compressedResult = 
settingsService.testDataSource(compressedRequest);
+        DataSourceTestResultVO expandedResult = 
settingsService.testDataSource(expandedRequest);
+
+        assertThat(compressedResult.isSuccess()).isFalse();
+        assertThat(compressedResult.getMessage()).contains("local or private 
address");
+        assertThat(expandedResult.isSuccess()).isFalse();
+        assertThat(expandedResult.getMessage()).contains("local or private 
address");
+    }
+
+    @Test
+    void testConnectionShouldRejectAlibabaCloudMetadataAddress() {
+        DataSourceTestDTO request = DataSourceTestDTO.builder()
+                .url("http://100.100.100.200/latest/meta-data/";)
+                .type("Prometheus")
+                .build();
+        DataSourceTestDTO ipv4MappedRequest = DataSourceTestDTO.builder()
+                .url("http://[::ffff:100.100.100.200]/latest/meta-data/";)
+                .type("Prometheus")
+                .build();
+
+        DataSourceTestResultVO result = 
settingsService.testDataSource(request);
+        DataSourceTestResultVO ipv4MappedResult = 
settingsService.testDataSource(ipv4MappedRequest);
+
+        assertThat(result.isSuccess()).isFalse();
+        assertThat(result.getMessage()).contains("local or private address");
+        assertThat(ipv4MappedResult.isSuccess()).isFalse();
+        assertThat(ipv4MappedResult.getMessage()).contains("local or private 
address");
+    }
+
     @Test
     void testConnectionShouldRejectIncompleteBasicAuthentication() {
         DataSourceTestResultVO result = 
settingsService.testDataSource(DataSourceTestDTO.builder()

Reply via email to