You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@impala.apache.org by mi...@apache.org on 2026/09/30 17:12:54 UTC

(impala) 02/05: IMPALA-15345: Resolve '..' segments before trusted_jar_paths check

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

MikaelSmith pushed a commit to branch branch-4.5.3
in repository https://gitbox.apache.org/repos/asf/impala.git

commit 8cfaf9b5a62522b409ed6a52ec0d322f6b4c2407
Author: Michael Smith <mi...@cloudera.com>
AuthorDate: Fri Sep 11 16:16:26 2026 -0700

    IMPALA-15345: Resolve '..' segments before trusted_jar_paths check
    
    BackendConfig.isJarPathAllowed() compared the raw driver.url / HDFS
    location string against the configured --trusted_jar_paths prefixes
    using String#startsWith(). That string is later turned into a Hadoop
    Path and resolved by the filesystem, which collapses '.' and '..'
    segments. A URL such as '<trusted_dir>/../../secret.jar' therefore
    passed the prefix check while actually resolving to a JAR outside the
    trusted directory, letting Impala load and execute attacker-controlled
    code in impalad.
    
    Resolve both the candidate path and each allowlist entry through
    Hadoop's Path class (which performs the same '.'/'..' collapsing)
    before comparing, and require the resolved path to be nested inside,
    or equal to, a resolved trusted directory rather than merely share a
    string prefix.
    
    Testing:
    - Added BackendConfigTest cases covering '..'-escape rejection,
      legitimate in-directory '..'/'.' usage, sibling-directory-name
      spoofing, and malformed allowlist entries.
    
    Change-Id: I4288aba344d7d0cd07c9fb8281fc8d3c0490e432
    Assisted-by: Claude Sonnet 5 (GitHub Copilot)
---
 .../org/apache/impala/service/BackendConfig.java   | 44 ++++++++++++++++++--
 .../apache/impala/service/BackendConfigTest.java   | 47 ++++++++++++++++++++++
 2 files changed, 88 insertions(+), 3 deletions(-)

diff --git a/fe/src/main/java/org/apache/impala/service/BackendConfig.java b/fe/src/main/java/org/apache/impala/service/BackendConfig.java
index e7d9bddd5..68a1a0234 100644
--- a/fe/src/main/java/org/apache/impala/service/BackendConfig.java
+++ b/fe/src/main/java/org/apache/impala/service/BackendConfig.java
@@ -23,6 +23,7 @@ import static org.apache.hadoop.fs.CommonConfigurationKeysPublic
 import java.util.Scanner;
 
 import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.security.authentication.util.KerberosName;
 import org.apache.impala.analysis.SqlScanner;
 import org.apache.impala.thrift.TBackendGflags;
@@ -566,19 +567,56 @@ public class BackendConfig {
   }
 
   /**
-   * Returns true if 'path' starts with one of the allowed prefixes configured in
-   * --trusted_jar_paths. An empty allowlist (the default) disables jar
+   * Returns true if 'path', once resolved the same way Hadoop's Path class resolves
+   * it (collapsing '.' and '..' segments), is inside one of the directories
+   * configured in --trusted_jar_paths. An empty allowlist (the default) disables jar
    * loading from table properties entirely.
+   *
+   * 'path' must be resolved (rather than compared as a raw string) because the caller
+   * eventually turns it into a Hadoop Path before reading the file, and Hadoop
+   * resolves '..' segments at that point. Comparing the raw, unresolved string would
+   * let a path like '<trusted_dir>/../../secret.jar' pass the allowlist check while
+   * actually resolving to a location outside the trusted directory.
    */
   public boolean isJarPathAllowed(String path) {
     String allowlist = backendCfg_.trusted_jar_paths;
     if (Strings.isNullOrEmpty(allowlist) || Strings.isNullOrEmpty(path)) return false;
+    String resolvedPath = resolveJarPath(path);
+    if (resolvedPath == null) return false;
     try (Scanner scanner = new Scanner(allowlist).useDelimiter(",")) {
       while (scanner.hasNext()) {
         String prefix = scanner.next().trim();
-        if (!prefix.isEmpty() && path.startsWith(prefix)) return true;
+        if (prefix.isEmpty()) continue;
+        String resolvedPrefix = resolveJarPath(prefix);
+        if (resolvedPrefix != null && isWithinTrustedDir(resolvedPath, resolvedPrefix)) {
+          return true;
+        }
       }
     }
     return false;
   }
+
+  /**
+   * Resolves 'rawPath' via Hadoop's Path class so that '.' and '..' segments are
+   * collapsed the same way they will be when the path is later used to read a file.
+   * Returns null if 'rawPath' cannot be parsed as a Path.
+   */
+  private static String resolveJarPath(String rawPath) {
+    try {
+      return new Path(rawPath).toString();
+    } catch (IllegalArgumentException e) {
+      return null;
+    }
+  }
+
+  /**
+   * Returns true if resolved path 'path' is exactly the resolved directory
+   * 'trustedDir', or is nested inside it. Both arguments must already be resolved via
+   * resolveJarPath().
+   */
+  private static boolean isWithinTrustedDir(String path, String trustedDir) {
+    if (path.equals(trustedDir)) return true;
+    String trustedDirWithSlash = trustedDir.endsWith("/") ? trustedDir : trustedDir + "/";
+    return path.startsWith(trustedDirWithSlash);
+  }
 }
diff --git a/fe/src/test/java/org/apache/impala/service/BackendConfigTest.java b/fe/src/test/java/org/apache/impala/service/BackendConfigTest.java
index f56355010..a4f161887 100644
--- a/fe/src/test/java/org/apache/impala/service/BackendConfigTest.java
+++ b/fe/src/test/java/org/apache/impala/service/BackendConfigTest.java
@@ -111,4 +111,51 @@ public class BackendConfigTest {
     BackendConfig cfg = configWith(",");
     assertFalse(cfg.isJarPathAllowed("/trusted/foo.jar"));
   }
+
+  // --- isJarPathAllowed: path traversal ('..') resolution ---
+
+  @Test
+  public void testIsJarPathAllowed_dotDotEscapesTrustedDir_returnsFalse() {
+    // Raw string starts with the trusted prefix, but resolving '..' segments (the
+    // same way the caller's Hadoop Path will) lands outside the trusted directory.
+    BackendConfig cfg = configWith("/trusted/");
+    assertFalse(cfg.isJarPathAllowed("/trusted/../../secret/foo.jar"));
+  }
+
+  @Test
+  public void testIsJarPathAllowed_dotDotEscapesTrustedDirWithScheme_returnsFalse() {
+    BackendConfig cfg = configWith("hdfs://nn/trusted/");
+    assertFalse(cfg.isJarPathAllowed("hdfs://nn/trusted/../../secret/foo.jar"));
+  }
+
+  @Test
+  public void testIsJarPathAllowed_dotDotStaysWithinTrustedDir_returnsTrue() {
+    // '..' segments that resolve to a location still inside the trusted directory
+    // are legitimate and should be allowed.
+    BackendConfig cfg = configWith("/trusted/");
+    assertTrue(cfg.isJarPathAllowed("/trusted/sub/../foo.jar"));
+  }
+
+  @Test
+  public void testIsJarPathAllowed_dotSegmentIsNoOp_returnsTrue() {
+    BackendConfig cfg = configWith("/trusted/");
+    assertTrue(cfg.isJarPathAllowed("/trusted/./foo.jar"));
+  }
+
+  @Test
+  public void testIsJarPathAllowed_siblingDirWithSamePrefixSpelling_returnsFalse() {
+    // A directory named similarly to the trusted one (but not nested inside it) must
+    // not be treated as trusted just because the raw string starts with the prefix.
+    BackendConfig cfg = configWith("/trusted");
+    assertFalse(cfg.isJarPathAllowed("/trusted-evil/foo.jar"));
+  }
+
+  @Test
+  public void testIsJarPathAllowed_unparsablePrefix_isSkipped() {
+    // A malformed allowlist entry must not be able to match everything; it should be
+    // skipped, and a valid subsequent entry should still be usable.
+    BackendConfig cfg = configWith("::not a valid path::,/trusted/");
+    assertTrue(cfg.isJarPathAllowed("/trusted/foo.jar"));
+    assertFalse(cfg.isJarPathAllowed("/untrusted/foo.jar"));
+  }
 }