Copilot commented on code in PR #11354:
URL: https://github.com/apache/gravitino/pull/11354#discussion_r3340049543


##########
core/src/main/java/org/apache/gravitino/job/JobManager.java:
##########
@@ -808,6 +809,7 @@ static String fetchFileFromUri(String uri, File stagingDir, 
int timeoutInMs) {
         case "http":
         case "https":
         case "ftp":
+          validateRemoteUri(fileUri);
           FileUtils.copyURLToFile(fileUri.toURL(), destFile, timeoutInMs, 
timeoutInMs);
           break;

Review Comment:
   `validateRemoteUri()` resolves the hostname, but 
`FileUtils.copyURLToFile(fileUri.toURL(), ...)` will perform its own DNS 
resolution again. That means DNS rebinding/TOCTOU can bypass this check if the 
hostname’s A/AAAA records change between validation and connection. Consider 
connecting using the validated `InetAddress` (or otherwise pinning the resolved 
IP) to make the validation effective at request time.



##########
core/src/main/java/org/apache/gravitino/job/JobManager.java:
##########
@@ -830,6 +832,47 @@ static String fetchFileFromUri(String uri, File 
stagingDir, int timeoutInMs) {
     }
   }
 
+  /**
+   * Resolves the host in the given URI and rejects addresses that should not 
be reachable from the
+   * server (loopback, link-local, RFC-1918 private ranges, cloud metadata 
endpoints). This is a
+   * defence-in-depth measure against Server-Side Request Forgery (SSRF).
+   */
+  @VisibleForTesting
+  static void validateRemoteUri(URI uri) throws IOException {
+    String host = uri.getHost();
+    if (host == null) {
+      throw new IllegalArgumentException("URI has no host: " + uri);
+    }
+    InetAddress[] addresses = InetAddress.getAllByName(host);
+    for (InetAddress address : addresses) {
+      if (isBlockedAddress(address)) {
+        throw new IllegalArgumentException(
+            String.format(
+                "URI '%s' resolves to blocked address %s, access denied (SSRF 
prevention)",
+                uri, address.getHostAddress()));
+      }
+    }
+  }
+
+  private static boolean isBlockedAddress(InetAddress address) {
+    // Covers loopback (127.x.x.x / ::1), link-local (169.254.x.x / fe80::/10 
— includes AWS/GCP/
+    // Azure metadata), RFC-1918 private (10.x / 172.16-31.x / 192.168.x), 
multicast, unspecified.
+    if (address.isLoopbackAddress()
+        || address.isLinkLocalAddress()
+        || address.isSiteLocalAddress()
+        || address.isMulticastAddress()
+        || address.isAnyLocalAddress()) {
+      return true;
+    }
+    // Alibaba Cloud / Oracle Cloud metadata endpoint: 100.100.100.200
+    byte[] b = address.getAddress();
+    return b.length == 4
+        && (b[0] & 0xFF) == 100
+        && (b[1] & 0xFF) == 100
+        && (b[2] & 0xFF) == 100
+        && (b[3] & 0xFF) == 200;

Review Comment:
   `isBlockedAddress()` relies on `isSiteLocalAddress()` for “private” ranges, 
but that does not cover IPv6 Unique Local Addresses (RFC 4193, `fc00::/7`). As 
a result, URIs resolving to `fc00::/7` can still pass validation, which 
undermines the stated SSRF protection for private/internal networks.



##########
core/src/test/java/org/apache/gravitino/job/TestJobManager.java:
##########
@@ -914,6 +915,72 @@ public void 
testFetchFileFromUriWithMissingLocalFileShouldFail() throws IOExcept
         RuntimeException.class, () -> JobManager.fetchFileFromUri(uri, 
stagingDir, 1000));
   }
 
+  @Test
+  public void testFetchFileFromUriSsrfBlocked() {
+    File stagingDir = new File(testStagingDir);
+    Assertions.assertTrue(stagingDir.mkdirs() || stagingDir.exists());
+
+    // Loopback address
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () -> JobManager.fetchFileFromUri("http://127.0.0.1:8090/configs";, 
stagingDir, 1000));
+
+    // AWS / GCP / Azure cloud-metadata endpoint (link-local 169.254.x.x)
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () ->
+            JobManager.fetchFileFromUri(
+                "http://169.254.169.254/latest/meta-data/";, stagingDir, 1000));
+

Review Comment:
   Same as above: this should verify the failure is due to SSRF validation 
(exception cause/message), not just that the fetch fails for any reason.



##########
core/src/test/java/org/apache/gravitino/job/TestJobManager.java:
##########
@@ -914,6 +915,72 @@ public void 
testFetchFileFromUriWithMissingLocalFileShouldFail() throws IOExcept
         RuntimeException.class, () -> JobManager.fetchFileFromUri(uri, 
stagingDir, 1000));
   }
 
+  @Test
+  public void testFetchFileFromUriSsrfBlocked() {
+    File stagingDir = new File(testStagingDir);
+    Assertions.assertTrue(stagingDir.mkdirs() || stagingDir.exists());
+
+    // Loopback address
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () -> JobManager.fetchFileFromUri("http://127.0.0.1:8090/configs";, 
stagingDir, 1000));
+
+    // AWS / GCP / Azure cloud-metadata endpoint (link-local 169.254.x.x)
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () ->
+            JobManager.fetchFileFromUri(
+                "http://169.254.169.254/latest/meta-data/";, stagingDir, 1000));
+
+    // RFC-1918 private range
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () -> JobManager.fetchFileFromUri("http://192.168.1.1/";, stagingDir, 
1000));
+
+    // Alibaba Cloud / Oracle Cloud metadata endpoint
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () -> JobManager.fetchFileFromUri("http://100.100.100.200/";, 
stagingDir, 1000));
+  }

Review Comment:
   Same issue: this should assert the fetch failed specifically due to SSRF 
prevention (cause/message), not simply because the endpoint is unreachable.



##########
core/src/test/java/org/apache/gravitino/job/TestJobManager.java:
##########
@@ -914,6 +915,72 @@ public void 
testFetchFileFromUriWithMissingLocalFileShouldFail() throws IOExcept
         RuntimeException.class, () -> JobManager.fetchFileFromUri(uri, 
stagingDir, 1000));
   }
 
+  @Test
+  public void testFetchFileFromUriSsrfBlocked() {
+    File stagingDir = new File(testStagingDir);
+    Assertions.assertTrue(stagingDir.mkdirs() || stagingDir.exists());
+
+    // Loopback address
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () -> JobManager.fetchFileFromUri("http://127.0.0.1:8090/configs";, 
stagingDir, 1000));
+

Review Comment:
   This assertion only checks that *some* `RuntimeException` is thrown. Without 
inspecting the cause/message, the test could still pass if the HTTP request 
fails for an unrelated reason (e.g., connection refused) and would not actually 
prove SSRF blocking is enforced by `validateRemoteUri()`.



##########
core/src/test/java/org/apache/gravitino/job/TestJobManager.java:
##########
@@ -914,6 +915,72 @@ public void 
testFetchFileFromUriWithMissingLocalFileShouldFail() throws IOExcept
         RuntimeException.class, () -> JobManager.fetchFileFromUri(uri, 
stagingDir, 1000));
   }
 
+  @Test
+  public void testFetchFileFromUriSsrfBlocked() {
+    File stagingDir = new File(testStagingDir);
+    Assertions.assertTrue(stagingDir.mkdirs() || stagingDir.exists());
+
+    // Loopback address
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () -> JobManager.fetchFileFromUri("http://127.0.0.1:8090/configs";, 
stagingDir, 1000));
+
+    // AWS / GCP / Azure cloud-metadata endpoint (link-local 169.254.x.x)
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () ->
+            JobManager.fetchFileFromUri(
+                "http://169.254.169.254/latest/meta-data/";, stagingDir, 1000));
+
+    // RFC-1918 private range
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () -> JobManager.fetchFileFromUri("http://192.168.1.1/";, stagingDir, 
1000));

Review Comment:
   Same issue: this assertion should confirm the exception is thrown by the 
SSRF validation (cause/message) to avoid a false positive if the network call 
fails for unrelated reasons.



##########
core/src/test/java/org/apache/gravitino/job/TestJobManager.java:
##########
@@ -914,6 +915,72 @@ public void 
testFetchFileFromUriWithMissingLocalFileShouldFail() throws IOExcept
         RuntimeException.class, () -> JobManager.fetchFileFromUri(uri, 
stagingDir, 1000));
   }
 
+  @Test
+  public void testFetchFileFromUriSsrfBlocked() {
+    File stagingDir = new File(testStagingDir);
+    Assertions.assertTrue(stagingDir.mkdirs() || stagingDir.exists());
+
+    // Loopback address
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () -> JobManager.fetchFileFromUri("http://127.0.0.1:8090/configs";, 
stagingDir, 1000));
+
+    // AWS / GCP / Azure cloud-metadata endpoint (link-local 169.254.x.x)
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () ->
+            JobManager.fetchFileFromUri(
+                "http://169.254.169.254/latest/meta-data/";, stagingDir, 1000));
+
+    // RFC-1918 private range
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () -> JobManager.fetchFileFromUri("http://192.168.1.1/";, stagingDir, 
1000));
+
+    // Alibaba Cloud / Oracle Cloud metadata endpoint
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () -> JobManager.fetchFileFromUri("http://100.100.100.200/";, 
stagingDir, 1000));
+  }
+
+  @Test
+  public void testValidateRemoteUri() throws Exception {
+    // Loopback
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> JobManager.validateRemoteUri(new URI("http://127.0.0.1/";)));
+
+    // Link-local (cloud metadata)
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> JobManager.validateRemoteUri(new 
URI("http://169.254.169.254/";)));
+
+    // RFC-1918 private 10.x.x.x
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> JobManager.validateRemoteUri(new URI("http://10.0.0.1/";)));
+
+    // RFC-1918 private 172.16.x.x
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> JobManager.validateRemoteUri(new URI("http://172.16.0.1/";)));
+
+    // RFC-1918 private 192.168.x.x
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> JobManager.validateRemoteUri(new URI("http://192.168.0.1/";)));
+

Review Comment:
   `validateRemoteUri()` is intended to block private/internal addresses, but 
the test suite doesn’t currently cover IPv6 Unique Local Addresses 
(`fc00::/7`). Adding a ULA case helps prevent regressions once IPv6 
private-range blocking is implemented.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to