jerryshao commented on code in PR #13721:
URL: https://github.com/apache/gravitino/pull/13721#discussion_r4236231208


##########
catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogOperations.java:
##########
@@ -1925,6 +1932,172 @@ void testTrailSlash() throws IOException {
     }
   }
 
+  @ParameterizedTest
+  @ValueSource(
+      strings = {
+        "..",
+        "../outside",
+        "/../outside",
+        "/dir/../../outside",
+        "dir/../file",
+        " /../outside ",
+        "/..\\outside",
+        "\\..\\outside",
+        "/dir\\..\\outside",
+        "/file\u0000name",
+        "\u0000file",
+        "/file\u0000"
+      })
+  void testRejectUnsafeSubPaths(String subPath) throws IOException {
+    try (FilesetCatalogOperations operations =
+        pathValidationOperations(mockFilesetForPathValidation())) {
+      Assertions.assertThrows(
+          IllegalArgumentException.class,
+          () -> operations.getFileLocation(PATH_VALIDATION_FILESET, subPath, 
null));
+      Mockito.verify(operations, 
Mockito.never()).getFileSystemWithCache(any(), any());

Review Comment:
   [Nit] This assertion cannot fail.
   
   `getFileLocation` never touches a filesystem on any path - it only reads 
`fileset.storageLocations()`, builds `Path` objects and validates - so 
`verify(never()).getFileSystemWithCache(...)` holds regardless of whether the 
validation works. Same for the copy at line 1970.
   
   The assertion is meaningful in `testListFilesCannotReachSiblingSentinel` 
(line 2075), where `listFiles` would otherwise reach the filesystem; here it 
reads as a guarantee the test is not actually providing. Suggest dropping it 
from these two tests, or asserting the exception message instead so the test 
distinguishes the backslash/NUL rejection from the `..` rejection from the 
boundary rejection - right now all three parameter groups would pass even if a 
single over-broad check swallowed them all.
   
   Verified by: read the whole method body at 
FilesetCatalogOperations.java:1797-1872 in this checkout; there is no 
filesystem access before the return.



##########
api/src/main/java/org/apache/gravitino/file/FilesetCatalog.java:
##########
@@ -249,10 +255,16 @@ default String getFileLocation(NameIdentifier ident, 
String subPath)
    * Get the actual location of a file or directory based on the storage 
location of Fileset and the
    * sub path by the location name.
    *
+   * <p>Leading and trailing whitespace in the sub path is trimmed. An empty 
path or {@code /}
+   * refers to the selected storage location. A leading slash is relative to 
that location.
+   *
    * @param ident A fileset identifier.
    * @param subPath The sub path to the file or directory.
    * @param locationName The location name. If null, the default location will 
be used.
    * @return The actual location of the file or directory.
+   * @throws IllegalArgumentException If the sub path contains a complete 
{@code ..} path segment, a
+   *     backslash, or a NUL character ({@code U+0000}), or its normalized 
path is outside the
+   *     selected storage location.

Review Comment:
   [Nit] The file-listing contract did not get the same treatment.
   
   Both `getFileLocation` overloads here and both OpenAPI operations now 
describe the constraints, but `FilesetFileOps#listFiles` - the Java-side 
declaration of the other endpoint that validates the sub path - still documents 
`@param subPath The sub path under the fileset.` with no constraints and no 
`IllegalArgumentException` 
(core/src/main/java/org/apache/gravitino/catalog/FilesetFileOps.java:34-45).
   
   It is an internal interface ("not exposed to public client APIs"), so this 
is low priority, but it is the only remaining place where the new behaviour is 
undocumented, and copying the two sentences added here would close the gap.
   
   Verified by: grepped for `FileInfo[] listFiles` across `api/`, `core/` and 
`catalogs/` - `FilesetFileOps:42` is the sole declaration, and it is unchanged 
in this PR.



##########
catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java:
##########
@@ -1796,11 +1796,18 @@ private String failureCategory(Throwable throwable) {
 
   private String getFileLocation(Fileset fileset, String subPath, String 
locationName) {
     Preconditions.checkArgument(subPath != null, "subPath must not be null");
-    String processedSubPath;
-    if (!subPath.trim().isEmpty() && !subPath.trim().startsWith(SLASH)) {
-      processedSubPath = SLASH + subPath.trim();
-    } else {
-      processedSubPath = subPath.trim();
+    String processedSubPath = subPath.trim();
+    // Validate before constructing a Hadoop Path, which normalizes away 
parent directory segments.
+    // Backslashes can be interpreted as separators by a client on a different 
operating system.
+    Preconditions.checkArgument(
+        subPath.indexOf('\\') < 0 && subPath.indexOf('\0') < 0,
+        "subPath must not contain backslashes or null characters");

Review Comment:
   [Question] Is breaking sub paths that contain a backslash an accepted cost?
   
   A backslash is a legal character in POSIX file names, HDFS paths and S3/GCS 
object keys, and before this change such a path was addressable: 
`getFileLocation` only concatenated, so `/dir\name.txt` resolved to the key 
`prefix/dir\name.txt`. After this change every `getFileLocation`/`listFiles` 
call for such an object fails with HTTP 400, and there is no way to reach it - 
including through GVFS, where `BaseGVFSOperations#getActualFilePath` 
(clients/filesystem-hadoop3/src/main/java/org/apache/gravitino/filesystem/hadoop/BaseGVFSOperations.java:526)
 hands the sub path through verbatim because `Path.WINDOWS` is false on a Linux 
client, so the backslash survives.
   
   Note that the backslash alone cannot cause server-side traversal: on a Linux 
server `/dir\..\outside` is one literal segment and the new normalized-ancestor 
check already keeps it inside the storage location. The ban only defends a 
client that would re-split on `\`, which is the right instinct, but it is a 
silent data-access regression for existing filesets rather than a security fix.
   
   If the trade-off is intended, please call it out explicitly in the PR's 
user-facing-change section (today it only says "unsupported sub-paths") and in 
the release notes, so operators with such keys learn about it before upgrading. 
If it is not worth the break, the boundary check on its own is already 
sufficient server-side.
   
   Verified by: read the pre-change concatenation in `git diff 
origin/main...HEAD` for this method, traced `Path(String)`/`normalizePath` to 
confirm `\` is not a separator when `Path.WINDOWS` is false, and read the GVFS 
sub-path derivation at the line cited above.



##########
docs/open-api/filesets.yaml:
##########
@@ -235,6 +247,8 @@ paths:
       responses:
         "200":
           $ref: "#/components/responses/FileInfoListResponse"
+        "400":
+          $ref: "#/components/responses/InvalidSubPathErrorResponse"

Review Comment:
   [Nit] Adding `400` here makes the neighbouring `404` description 
self-contradictory.
   
   Line 253 still reads "the target fileset or subPath or locationName does not 
exist", but a sub path that simply does not exist has never produced a 404 on 
this endpoint: `FilesetCatalogOperations#listFiles` throws 
`IllegalArgumentException` 
(catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java:421-423,
 "Path %s does not exist in fileset %s"), which `FilesetExceptionHandler` turns 
into 400. The PR's own `testListFilesCannotReachSiblingSentinel` relies on this 
for the `/../missing` case.
   
   Since this PR is specifically documenting the error responses, suggest 
narrowing the `404` description to the fileset and the location name, and 
extending the `400` description to cover a non-existent sub path alongside an 
invalid one.
   
   Verified by: read the throw site and 
`ExceptionHandlers$FilesetExceptionHandler` in this checkout; 
`IllegalArgumentException` is the first branch and returns 
`Utils.illegalArguments`.



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