zabetak commented on code in PR #6851:
URL: https://github.com/apache/hive/pull/6851#discussion_r4347934616


##########
ql/src/java/org/apache/hadoop/hive/ql/io/SymbolicInputFormat.java:
##########
@@ -98,4 +102,58 @@ public void rework(HiveConf job, MapredWork work) throws 
IOException {
       work.getMapWork().removePathToPartitionInfo(toRemove);
     }
   }
+
+  /**
+   * Expands a symlink file line, accepting only matches under the symlink 
file's directory or an allowed root.
+   */
+  static List<Path> resolveTargets(Configuration conf, Path symlinkFile, 
List<Path> allowedRoots, String line)
+      throws IOException {
+    // Qualified against the default filesystem, so no filesystem is created 
for an arbitrary scheme.
+    FileSystem defaultFs = FileSystem.get(conf);
+    List<Path> roots = new ArrayList<>(allowedRoots);
+    roots.add(qualify(defaultFs, symlinkFile.getParent()));
+    Path pattern = qualify(defaultFs, new Path(line));
+    // Matches are checked too, as a glob can climb out of the root.
+    checkAllowed(pattern, roots);
+    FileStatus[] statuses = pattern.getFileSystem(conf).globStatus(pattern);
+    if (statuses == null) {
+      return Collections.emptyList();
+    }
+    List<Path> matches = new ArrayList<>();
+    for (FileStatus status : statuses) {
+      Path match = qualify(defaultFs, status.getPath());
+      checkAllowed(match, roots);
+      // FileInputFormat globs the match again, so it must not contain glob 
metacharacters.
+      if (GLOB_METACHARS.matcher(match.toUri().getPath()).find()) {
+        throw new IOException("Symlink target " + match + " contains glob 
metacharacters");
+      }
+      matches.add(match);
+    }
+    return matches;
+  }
+
+  private static void checkAllowed(Path target, List<Path> roots) throws 
IOException {
+    for (Path root : roots) {
+      if (FileUtils.isPathWithinSubtree(target, root)) {
+        return;
+      }
+    }
+    throw new IOException("Symlink target " + target + " is outside of the 
allowed locations. Additional locations"
+        + " can be configured with " + 
ConfVars.HIVE_SYMLINK_ALLOWED_TARGET_PATHS.varname);
+  }
+
+  static List<Path> getAllowedRoots(Configuration conf) throws IOException {
+    List<Path> allowedRoots = new ArrayList<>();
+    for (String root : HiveConf.getTrimmedStringsVar(conf, 
ConfVars.HIVE_SYMLINK_ALLOWED_TARGET_PATHS)) {
+      if (!root.isEmpty()) {
+        Path rootPath = new Path(root);
+        allowedRoots.add(qualify(rootPath.getFileSystem(conf), rootPath));
+      }
+    }
+    return allowedRoots;
+  }
+
+  private static Path qualify(FileSystem fs, Path path) {
+    return new Path(path.makeQualified(fs.getUri(), 
fs.getWorkingDirectory()).toUri().normalize());
+  }

Review Comment:
   Do we really need this wrapper method? Can we simply inline 
`path.makeQualified(fs.getUri(), fs.getWorkingDirectory())` where necessary?
   The implementation of `Path#makeQualified(java.net.URI, 
org.apache.hadoop.fs.Path)` seems to already handle uris and normalization?
   



##########
ql/src/java/org/apache/hadoop/hive/ql/io/SymbolicInputFormat.java:
##########
@@ -98,4 +102,58 @@ public void rework(HiveConf job, MapredWork work) throws 
IOException {
       work.getMapWork().removePathToPartitionInfo(toRemove);
     }
   }
+
+  /**
+   * Expands a symlink file line, accepting only matches under the symlink 
file's directory or an allowed root.
+   */
+  static List<Path> resolveTargets(Configuration conf, Path symlinkFile, 
List<Path> allowedRoots, String line)

Review Comment:
   I think we can drop the `symlinkFile` parameter and the logic associated 
with it. If a symlinkFile content points to other files under the same 
directory it seems like that we are gonna end up with a circular dependency. 
Dropping this logic would make the code more readable and a bit more efficient 
since we will not have to copy the `allowedRoots` for every line in the file.



##########
ql/src/test/org/apache/hadoop/hive/ql/io/TestSymlinkTextInputFormat.java:
##########
@@ -364,4 +372,185 @@ private void writeSymlinkFile(Path symlinkFile, 
Path...paths)
     }
     writer.close();
   }
+
+  @Test
+  public void testTargetOutsideOfAllowedPaths() throws IOException {
+    writeTextFile(new Path(dataDir1, "file1"), "dir1_file1_line1\n");
+    writeSymlinkFile(new Path(symlinkDir, "symlink_file"), new Path(dataDir1, 
"file1"));
+
+    SymlinkTextInputFormat inputFormat = new SymlinkTextInputFormat();
+    FileInputFormat.setInputPaths(job, symlinkDir);
+
+    assertRejected(
+        assertThrows(IOException.class, () -> inputFormat.getSplits(job, 
2)).getCause()
+    );
+    assertRejected(
+        assertThrows(IOException.class, () -> 
inputFormat.getContentSummary(symlinkDir, job)).getCause()
+    );
+  }
+
+  @Test
+  public void testTargetEscapingAllowedPath() throws IOException {
+    writeTextFile(new Path(dataDir2, "file2"), "dir2_file2_line1\n");
+    writeTextFile(new Path(symlinkDir, "symlink_file"), dataDir1 + 
"/../datadir2/file2\n");
+    job.set(ConfVars.HIVE_SYMLINK_ALLOWED_TARGET_PATHS.varname, 
dataDir1.toString());
+
+    SymlinkTextInputFormat inputFormat = new SymlinkTextInputFormat();
+    FileInputFormat.setInputPaths(job, symlinkDir);
+
+    assertRejected(assertThrows(IOException.class, () -> 
inputFormat.getSplits(job, 2)).getCause());
+  }
+
+  /** Rejected before any FileSystem is created for the unknown scheme. */
+  @Test
+  public void testTargetOnForeignFileSystem() throws IOException {
+    writeTextFile(new Path(symlinkDir, "symlink_file"), 
"unknownfs://custom:1234/abc\n");

Review Comment:
   How does this test verify that the rejection happens before the FileSystem 
creation?



##########
ql/src/java/org/apache/hadoop/hive/ql/io/SymbolicInputFormat.java:
##########
@@ -98,4 +102,58 @@ public void rework(HiveConf job, MapredWork work) throws 
IOException {
       work.getMapWork().removePathToPartitionInfo(toRemove);
     }
   }
+
+  /**
+   * Expands a symlink file line, accepting only matches under the symlink 
file's directory or an allowed root.
+   */
+  static List<Path> resolveTargets(Configuration conf, Path symlinkFile, 
List<Path> allowedRoots, String line)
+      throws IOException {
+    // Qualified against the default filesystem, so no filesystem is created 
for an arbitrary scheme.
+    FileSystem defaultFs = FileSystem.get(conf);

Review Comment:
   The callers of `resolveTargets` already have a `FileSystem` instance 
available. Why do we need to create a new one and not pass the one already 
available?



##########
ql/src/java/org/apache/hadoop/hive/ql/io/SymbolicInputFormat.java:
##########
@@ -98,4 +102,58 @@ public void rework(HiveConf job, MapredWork work) throws 
IOException {
       work.getMapWork().removePathToPartitionInfo(toRemove);
     }
   }
+
+  /**
+   * Expands a symlink file line, accepting only matches under the symlink 
file's directory or an allowed root.
+   */
+  static List<Path> resolveTargets(Configuration conf, Path symlinkFile, 
List<Path> allowedRoots, String line)
+      throws IOException {
+    // Qualified against the default filesystem, so no filesystem is created 
for an arbitrary scheme.
+    FileSystem defaultFs = FileSystem.get(conf);
+    List<Path> roots = new ArrayList<>(allowedRoots);
+    roots.add(qualify(defaultFs, symlinkFile.getParent()));
+    Path pattern = qualify(defaultFs, new Path(line));
+    // Matches are checked too, as a glob can climb out of the root.
+    checkAllowed(pattern, roots);
+    FileStatus[] statuses = pattern.getFileSystem(conf).globStatus(pattern);
+    if (statuses == null) {
+      return Collections.emptyList();
+    }
+    List<Path> matches = new ArrayList<>();
+    for (FileStatus status : statuses) {
+      Path match = qualify(defaultFs, status.getPath());
+      checkAllowed(match, roots);
+      // FileInputFormat globs the match again, so it must not contain glob 
metacharacters.
+      if (GLOB_METACHARS.matcher(match.toUri().getPath()).find()) {
+        throw new IOException("Symlink target " + match + " contains glob 
metacharacters");
+      }
+      matches.add(match);
+    }
+    return matches;
+  }
+
+  private static void checkAllowed(Path target, List<Path> roots) throws 
IOException {
+    for (Path root : roots) {
+      if (FileUtils.isPathWithinSubtree(target, root)) {
+        return;
+      }
+    }
+    throw new IOException("Symlink target " + target + " is outside of the 
allowed locations. Additional locations"
+        + " can be configured with " + 
ConfVars.HIVE_SYMLINK_ALLOWED_TARGET_PATHS.varname);
+  }
+
+  static List<Path> getAllowedRoots(Configuration conf) throws IOException {
+    List<Path> allowedRoots = new ArrayList<>();
+    for (String root : HiveConf.getTrimmedStringsVar(conf, 
ConfVars.HIVE_SYMLINK_ALLOWED_TARGET_PATHS)) {
+      if (!root.isEmpty()) {
+        Path rootPath = new Path(root);
+        allowedRoots.add(qualify(rootPath.getFileSystem(conf), rootPath));
+      }
+    }
+    return allowedRoots;

Review Comment:
   It appears that we shouldn't modify this list so better use Guava's 
`ImmutableList` or `Collections.unmodifiableList`



##########
ql/src/java/org/apache/hadoop/hive/ql/io/SymlinkTextInputFormat.java:
##########
@@ -192,8 +193,14 @@ private static void getTargetPathsFromSymlinksDirs(
                   fileSystem.open(symlink.getPath())));
           String line;
           while ((line = reader.readLine()) != null) {
-            targetPaths.add(new Path(line));
-            symlinkPaths.add(symlink.getPath());
+            List<Path> matches = resolveTargets(conf, symlink.getPath(), 
allowedRoots, line);
+            if (matches.isEmpty()) {
+              throw new IOException("Symlink target " + line + " does not 
match any path");
+            }

Review Comment:
   Should it fail or just turn into a noop? The behavior does not seem 
consistent with `SymbolicInputFormat#rework`.



##########
ql/src/java/org/apache/hadoop/hive/ql/io/SymbolicInputFormat.java:
##########
@@ -98,4 +102,58 @@ public void rework(HiveConf job, MapredWork work) throws 
IOException {
       work.getMapWork().removePathToPartitionInfo(toRemove);
     }
   }
+
+  /**
+   * Expands a symlink file line, accepting only matches under the symlink 
file's directory or an allowed root.
+   */
+  static List<Path> resolveTargets(Configuration conf, Path symlinkFile, 
List<Path> allowedRoots, String line)
+      throws IOException {
+    // Qualified against the default filesystem, so no filesystem is created 
for an arbitrary scheme.
+    FileSystem defaultFs = FileSystem.get(conf);
+    List<Path> roots = new ArrayList<>(allowedRoots);
+    roots.add(qualify(defaultFs, symlinkFile.getParent()));
+    Path pattern = qualify(defaultFs, new Path(line));
+    // Matches are checked too, as a glob can climb out of the root.
+    checkAllowed(pattern, roots);

Review Comment:
   1. Should we really have this check here? Can't we just keep the one inside 
the for-loop? 
   2. Is it possible to have a pattern that at this point is invalid but after 
glob expansion becomes valid?



##########
ql/src/java/org/apache/hadoop/hive/ql/io/SymlinkTextInputFormat.java:
##########
@@ -192,8 +193,14 @@ private static void getTargetPathsFromSymlinksDirs(
                   fileSystem.open(symlink.getPath())));
           String line;
           while ((line = reader.readLine()) != null) {
-            targetPaths.add(new Path(line));
-            symlinkPaths.add(symlink.getPath());

Review Comment:
   In the old code there was no glob expansion at this point and by navigating 
a bit the caller hierarchy I don't see  this triggering elsewhere. Does the new 
code add more capabilities & new functionality for this format?



##########
ql/src/java/org/apache/hadoop/hive/ql/io/SymbolicInputFormat.java:
##########
@@ -98,4 +102,58 @@ public void rework(HiveConf job, MapredWork work) throws 
IOException {
       work.getMapWork().removePathToPartitionInfo(toRemove);
     }
   }
+
+  /**
+   * Expands a symlink file line, accepting only matches under the symlink 
file's directory or an allowed root.
+   */
+  static List<Path> resolveTargets(Configuration conf, Path symlinkFile, 
List<Path> allowedRoots, String line)
+      throws IOException {
+    // Qualified against the default filesystem, so no filesystem is created 
for an arbitrary scheme.
+    FileSystem defaultFs = FileSystem.get(conf);
+    List<Path> roots = new ArrayList<>(allowedRoots);
+    roots.add(qualify(defaultFs, symlinkFile.getParent()));
+    Path pattern = qualify(defaultFs, new Path(line));
+    // Matches are checked too, as a glob can climb out of the root.
+    checkAllowed(pattern, roots);
+    FileStatus[] statuses = pattern.getFileSystem(conf).globStatus(pattern);

Review Comment:
   It seems that we already have a filesystem (i.e., `defaultFs`) why do we 
need to create it again?



##########
ql/src/test/org/apache/hadoop/hive/ql/io/TestSymlinkTextInputFormat.java:
##########
@@ -364,4 +372,185 @@ private void writeSymlinkFile(Path symlinkFile, 
Path...paths)
     }
     writer.close();
   }
+
+  @Test
+  public void testTargetOutsideOfAllowedPaths() throws IOException {
+    writeTextFile(new Path(dataDir1, "file1"), "dir1_file1_line1\n");
+    writeSymlinkFile(new Path(symlinkDir, "symlink_file"), new Path(dataDir1, 
"file1"));
+
+    SymlinkTextInputFormat inputFormat = new SymlinkTextInputFormat();
+    FileInputFormat.setInputPaths(job, symlinkDir);
+
+    assertRejected(
+        assertThrows(IOException.class, () -> inputFormat.getSplits(job, 
2)).getCause()
+    );
+    assertRejected(
+        assertThrows(IOException.class, () -> 
inputFormat.getContentSummary(symlinkDir, job)).getCause()
+    );
+  }
+
+  @Test
+  public void testTargetEscapingAllowedPath() throws IOException {
+    writeTextFile(new Path(dataDir2, "file2"), "dir2_file2_line1\n");
+    writeTextFile(new Path(symlinkDir, "symlink_file"), dataDir1 + 
"/../datadir2/file2\n");
+    job.set(ConfVars.HIVE_SYMLINK_ALLOWED_TARGET_PATHS.varname, 
dataDir1.toString());
+
+    SymlinkTextInputFormat inputFormat = new SymlinkTextInputFormat();
+    FileInputFormat.setInputPaths(job, symlinkDir);
+
+    assertRejected(assertThrows(IOException.class, () -> 
inputFormat.getSplits(job, 2)).getCause());
+  }
+
+  /** Rejected before any FileSystem is created for the unknown scheme. */
+  @Test
+  public void testTargetOnForeignFileSystem() throws IOException {
+    writeTextFile(new Path(symlinkDir, "symlink_file"), 
"unknownfs://custom:1234/abc\n");
+
+    SymlinkTextInputFormat inputFormat = new SymlinkTextInputFormat();
+    FileInputFormat.setInputPaths(job, symlinkDir);
+
+    assertRejected(assertThrows(IOException.class, () -> 
inputFormat.getSplits(job, 2)).getCause());
+  }
+
+  @Test
+  public void testReworkKeepsTargetNextToSymlinkFile() throws IOException {

Review Comment:
   What is this test about?



##########
ql/src/java/org/apache/hadoop/hive/ql/io/SymbolicInputFormat.java:
##########
@@ -98,4 +102,58 @@ public void rework(HiveConf job, MapredWork work) throws 
IOException {
       work.getMapWork().removePathToPartitionInfo(toRemove);
     }
   }
+
+  /**
+   * Expands a symlink file line, accepting only matches under the symlink 
file's directory or an allowed root.
+   */
+  static List<Path> resolveTargets(Configuration conf, Path symlinkFile, 
List<Path> allowedRoots, String line)
+      throws IOException {
+    // Qualified against the default filesystem, so no filesystem is created 
for an arbitrary scheme.

Review Comment:
   Don't understand very well what the comment means.



##########
ql/src/java/org/apache/hadoop/hive/ql/io/SymbolicInputFormat.java:
##########
@@ -98,4 +102,58 @@ public void rework(HiveConf job, MapredWork work) throws 
IOException {
       work.getMapWork().removePathToPartitionInfo(toRemove);
     }
   }
+
+  /**
+   * Expands a symlink file line, accepting only matches under the symlink 
file's directory or an allowed root.
+   */
+  static List<Path> resolveTargets(Configuration conf, Path symlinkFile, 
List<Path> allowedRoots, String line)
+      throws IOException {
+    // Qualified against the default filesystem, so no filesystem is created 
for an arbitrary scheme.
+    FileSystem defaultFs = FileSystem.get(conf);
+    List<Path> roots = new ArrayList<>(allowedRoots);
+    roots.add(qualify(defaultFs, symlinkFile.getParent()));
+    Path pattern = qualify(defaultFs, new Path(line));
+    // Matches are checked too, as a glob can climb out of the root.
+    checkAllowed(pattern, roots);
+    FileStatus[] statuses = pattern.getFileSystem(conf).globStatus(pattern);
+    if (statuses == null) {
+      return Collections.emptyList();
+    }
+    List<Path> matches = new ArrayList<>();
+    for (FileStatus status : statuses) {
+      Path match = qualify(defaultFs, status.getPath());
+      checkAllowed(match, roots);
+      // FileInputFormat globs the match again, so it must not contain glob 
metacharacters.
+      if (GLOB_METACHARS.matcher(match.toUri().getPath()).find()) {
+        throw new IOException("Symlink target " + match + " contains glob 
metacharacters");
+      }
+      matches.add(match);
+    }
+    return matches;
+  }
+
+  private static void checkAllowed(Path target, List<Path> roots) throws 
IOException {
+    for (Path root : roots) {
+      if (FileUtils.isPathWithinSubtree(target, root)) {
+        return;
+      }
+    }
+    throw new IOException("Symlink target " + target + " is outside of the 
allowed locations. Additional locations"
+        + " can be configured with " + 
ConfVars.HIVE_SYMLINK_ALLOWED_TARGET_PATHS.varname);
+  }
+
+  static List<Path> getAllowedRoots(Configuration conf) throws IOException {
+    List<Path> allowedRoots = new ArrayList<>();
+    for (String root : HiveConf.getTrimmedStringsVar(conf, 
ConfVars.HIVE_SYMLINK_ALLOWED_TARGET_PATHS)) {
+      if (!root.isEmpty()) {
+        Path rootPath = new Path(root);
+        allowedRoots.add(qualify(rootPath.getFileSystem(conf), rootPath));

Review Comment:
   Have you considered what should happen if the some root path is not valid or 
doesn't exist. It seems that a misconfiguration of the property can make all 
Symbolic tables unusable till an HS2 restart happens.



-- 
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]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to