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));
}