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

davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 2e3c1953d5 [Fix][Zeta] Fix path traversal vulnerability in log file 
REST API endpoints (#10628)
2e3c1953d5 is described below

commit 2e3c1953d58bc9b467088e93667b9221dd1feba9
Author: David Zollo <[email protected]>
AuthorDate: Wed May 13 20:27:38 2026 +0800

    [Fix][Zeta] Fix path traversal vulnerability in log file REST API endpoints 
(#10628)
    
    Co-authored-by: github-actions[bot] 
<github-actions[bot]@users.noreply.github.com>
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../seatunnel/engine/e2e/joblog/JobLogIT.java      | 36 ++++++++++++++++++++++
 .../server/rest/RestHttpGetCommandProcessor.java   | 33 +++++++++++++++++---
 .../engine/server/rest/servlet/LogBaseServlet.java | 34 +++++++++++++++++---
 3 files changed, 94 insertions(+), 9 deletions(-)

diff --git 
a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/joblog/JobLogIT.java
 
b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/joblog/JobLogIT.java
index 611dc28623..7bf9b2405c 100644
--- 
a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/joblog/JobLogIT.java
+++ 
b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/joblog/JobLogIT.java
@@ -105,6 +105,42 @@ public class JobLogIT extends SeaTunnelEngineContainer {
         }
     }
 
+    @Test
+    public void testPathTraversalAttackPrevention() throws IOException, 
InterruptedException {
+        // Test path traversal with Unix-style relative path
+        Container.ExecResult result =
+                server.execInContainer(
+                        "sh",
+                        "-c",
+                        "curl --path-as-is -s -o /dev/null -w '%{http_code}' 
'http://localhost:8080/log/../../etc/passwd'");
+        Assertions.assertEquals(
+                "400",
+                result.getStdout().trim(),
+                "Path traversal attack with ../../etc/passwd should be blocked 
with HTTP 400");
+
+        // Test path traversal with deeper relative path
+        result =
+                server.execInContainer(
+                        "sh",
+                        "-c",
+                        "curl --path-as-is -s -o /dev/null -w '%{http_code}' 
'http://localhost:8080/log/../../../etc/shadow'");
+        Assertions.assertEquals(
+                "400",
+                result.getStdout().trim(),
+                "Path traversal attack with ../../../etc/shadow should be 
blocked with HTTP 400");
+
+        // Test path traversal via /logs endpoint (all node log)
+        result =
+                server.execInContainer(
+                        "sh",
+                        "-c",
+                        "curl --path-as-is -s -o /dev/null -w '%{http_code}' 
'http://localhost:8080/logs/../../etc/passwd'");
+        Assertions.assertEquals(
+                "400",
+                result.getStdout().trim(),
+                "Path traversal attack on /logs endpoint should be blocked 
with HTTP 400");
+    }
+
     @Test
     public void testJobLogFile() throws Exception {
         submitJobAndAssertResponse(
diff --git 
a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestHttpGetCommandProcessor.java
 
b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestHttpGetCommandProcessor.java
index b5212a9b3e..e742f0103f 100644
--- 
a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestHttpGetCommandProcessor.java
+++ 
b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/RestHttpGetCommandProcessor.java
@@ -344,14 +344,39 @@ public class RestHttpGetCommandProcessor extends 
HttpCommandProcessor<HttpGetCom
         }
     }
 
-    /** Prepare Log Response */
+    /**
+     * Prepares the current-node log response after enforcing the configured 
log directory boundary.
+     *
+     * <p>The requested log file is resolved to its canonical path before 
reading so that relative
+     * segments and symbolic links cannot escape the canonical log directory.
+     *
+     * @param httpGetCommand command used to send the HTTP response
+     * @param logPath configured log directory
+     * @param logName requested log file name from the request URI
+     */
     private void prepareLogResponse(HttpGetCommand httpGetCommand, String 
logPath, String logName) {
-        String logFilePath = logPath + "/" + logName;
+        String logFilePath = new File(logPath, logName).getPath();
         try {
-            String logContent = FileUtils.readFileToStr(new 
File(logFilePath).toPath());
+            String canonicalLogDir = new File(logPath).getCanonicalPath();
+            String canonicalFilePath = new 
File(logFilePath).getCanonicalPath();
+            if (!canonicalFilePath.startsWith(canonicalLogDir + File.separator)
+                    && !canonicalFilePath.equals(canonicalLogDir)) {
+                httpGetCommand.send400();
+                logger.warning(
+                        String.format(
+                                "Path traversal attempt blocked - Requested: 
%s, Resolved: %s, LogDir: %s",
+                                logName, canonicalFilePath, canonicalLogDir));
+                return;
+            }
+            String logContent = FileUtils.readFileToStr(new 
File(canonicalFilePath).toPath());
             this.prepareResponse(httpGetCommand, logContent);
+        } catch (IOException e) {
+            httpGetCommand.send400();
+            logger.warning(
+                    String.format(
+                            "Failed to resolve log file path: %s, error: %s",
+                            logFilePath, e.getMessage()));
         } catch (SeaTunnelRuntimeException e) {
-            // If the log file does not exist, return 400
             httpGetCommand.send400();
             logger.warning(
                     String.format("Log file content is empty, get log path : 
%s", logFilePath));
diff --git 
a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/LogBaseServlet.java
 
b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/LogBaseServlet.java
index c877586572..f79ca21224 100644
--- 
a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/LogBaseServlet.java
+++ 
b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/rest/servlet/LogBaseServlet.java
@@ -36,7 +36,17 @@ public class LogBaseServlet extends BaseServlet {
     public LogBaseServlet(NodeEngineImpl nodeEngine) {
         super(nodeEngine);
     }
-    /** Prepare Log Response */
+
+    /**
+     * Prepares the servlet log response after enforcing the configured log 
directory boundary.
+     *
+     * <p>The requested log file is resolved to its canonical path before 
reading so that relative
+     * segments and symbolic links cannot escape the canonical log directory.
+     *
+     * @param resp response used to return status and log content
+     * @param logPath configured log directory
+     * @param logName requested log file name from the request URI
+     */
     protected void prepareLogResponse(HttpServletResponse resp, String 
logPath, String logName) {
         if (StringUtils.isBlank(logPath)) {
             resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
@@ -44,12 +54,26 @@ public class LogBaseServlet extends BaseServlet {
                     "Log file path is empty, no log file path configured in 
the current configuration file");
             return;
         }
-        String logFilePath = logPath + "/" + logName;
+        String logFilePath = new File(logPath, logName).getPath();
         try {
-            String logContent = FileUtils.readFileToStr(new 
File(logFilePath).toPath());
+            String canonicalLogDir = new File(logPath).getCanonicalPath();
+            String canonicalFilePath = new 
File(logFilePath).getCanonicalPath();
+            if (!canonicalFilePath.startsWith(canonicalLogDir + File.separator)
+                    && !canonicalFilePath.equals(canonicalLogDir)) {
+                resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+                log.warn(
+                        "Path traversal attempt blocked - Requested: {}, 
Resolved: {}, LogDir: {}",
+                        logName,
+                        canonicalFilePath,
+                        canonicalLogDir);
+                return;
+            }
+            String logContent = FileUtils.readFileToStr(new 
File(canonicalFilePath).toPath());
             write(resp, logContent);
-        } catch (SeaTunnelRuntimeException | IOException e) {
-            // If the log file does not exist, return 400
+        } catch (IOException e) {
+            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            log.warn("Failed to resolve log file path: {}, error: {}", 
logFilePath, e.getMessage());
+        } catch (SeaTunnelRuntimeException e) {
             resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
             log.warn(String.format("Log file content is empty, get log path : 
%s", logFilePath));
         }

Reply via email to