Skip to content

[Fix][Zeta] Fix path traversal vulnerability in log file REST API endpoints#10628

Open
davidzollo wants to merge 4 commits intoapache:devfrom
davidzollo:fix/log-path-traversal
Open

[Fix][Zeta] Fix path traversal vulnerability in log file REST API endpoints#10628
davidzollo wants to merge 4 commits intoapache:devfrom
davidzollo:fix/log-path-traversal

Conversation

@davidzollo
Copy link
Copy Markdown
Contributor

@davidzollo davidzollo commented Mar 20, 2026

Purpose

Fix a path traversal vulnerability (CWE-22) in the log file REST API endpoints that allows arbitrary file read outside the configured log directory.

Affected Endpoints

  • /hazelcast/rest/maps/log/<path> (current node log)
  • /hazelcast/rest/maps/logs/<path> (all node log)

Both the legacy Hazelcast REST handler and the Jetty servlet code paths are affected.

Root Cause

The logName parameter extracted from the request URI is concatenated directly into a file path without any sanitization. An attacker can use path traversal sequences (e.g., ../../etc/passwd) to read arbitrary files on the server.

Fix

Add canonical path validation in prepareLogResponse() for both code paths:

  1. RestHttpGetCommandProcessor (legacy Hazelcast handler)
  2. LogBaseServlet (Jetty servlet base class)

The fix resolves the constructed file path to its canonical form via File.getCanonicalPath() and verifies it remains within the configured log directory boundary. Requests attempting to escape the log directory are rejected with HTTP 400.

Changed Files

  • seatunnel-engine/seatunnel-engine-server/.../rest/RestHttpGetCommandProcessor.java
  • seatunnel-engine/seatunnel-engine-server/.../rest/servlet/LogBaseServlet.java

CVE-2026-40119

@DanielCarter-stack
Copy link
Copy Markdown

Issue 1: Missing test cases for path traversal attacks

Location: seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/joblog/JobLogIT.java

Related context:

  • Modified classes: RestHttpGetCommandProcessor, LogBaseServlet
  • Existing tests: JobLogIT.java (lines 108-136)

Problem description:
The PR fixes a path traversal vulnerability, but no test cases were added to verify the fix's effectiveness. Although existing JobLogIT tests normal log reading functionality, it doesn't test attack scenarios (such as ../../etc/passwd).

Potential risks:

  • Cannot automatically verify security fixes through CI/CD
  • Future modifications may inadvertently break protection logic
  • Reduces confidence in fix effectiveness

Impact scope:

  • Direct impact: JobLogIT test suite
  • Indirect impact: Security vulnerability regression risk

Severity: MAJOR

Improvement suggestions:

// Add test method in JobLogIT.java:
@Test
public void testPathTraversalAttackPrevention() throws IOException, InterruptedException {
    // Test Unix path traversal
    Container.ExecResult result = server.execInContainer("sh", "-c",
        "curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/log/../../etc/passwd");
    Assertions.assertEquals("400", result.getStdout().trim(),
        "Path traversal attack should be blocked with HTTP 400");
    
    // Test Windows style path traversal (if applicable)
    result = server.execInContainer("sh", "-c",
        "curl -s -o /dev/null -w '%{http_code}' 'http://localhost:8080/log/..\\..\\..\\..\\etc\\passwd'");
    Assertions.assertEquals("400", result.getStdout().trim(),
        "Windows-style path traversal should also be blocked");
    
    // Test symlink attack (if operable in container)
    // server.execInContainer("sh", "-c", "ln -s /etc/passwd /tmp/seatunnel/logs/evil.log");
    // result = server.execInContainer("sh", "-c",
    //     "curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/log/evil.log");
    // Assertions.assertEquals("400", result.getStdout().trim());
}

Rationale: Security fixes must be verified through automated testing to prevent regression


Issue 2: Incomplete JavaDoc comments

Location:

  • RestHttpGetCommandProcessor.java:324
  • LogBaseServlet.java:40

Related context:

  • Method signature: private void prepareLogResponse(HttpGetCommand httpGetCommand, String logPath, String logName)
  • Parent class/interface: None
  • Callers: getCurrentNodeLog(), getAllNodeLog()

Problem description:
The modified method changed from JavaDoc-style comments (/** */) to single-line comments (//), without explaining:

  • Parameter meanings (logPath, logName)
  • Path validation logic
  • Security check details
  • Exception handling behavior

Potential risks:

  • Reduces code maintainability
  • Difficult for new developers to understand security logic
  • May lead to incorrect modifications

Impact scope:

  • Direct impact: Readability of prepareLogResponse() method
  • Indirect impact: Future code review and maintenance

Severity: MINOR

Improvement suggestions:

/**
 * Prepares log file response with path traversal protection.
 * 
 * <p>This method validates that the requested log file is within the configured
 * log directory boundary to prevent CWE-22 path traversal attacks.
 * 
 * <p>The validation is performed by:
 * <ol>
 *   <li>Resolving both log directory and requested file to canonical paths
 *        (which resolves all {@code ..} and symbolic links)</li>
 *   <li>Verifying the canonical file path starts with the canonical log directory
 *        followed by {@link File#separator}</li>
 * </ol>
 * 
 * <p>Requests attempting to access files outside the log directory are rejected
 * with HTTP 400 and a warning is logged.
 * 
 * @param httpGetCommand the HTTP GET command to send response
 * @param logPath the configured log directory path (must not be null/empty)
 * @param logName the requested log file name from URI parameter
 * @throws IOException if canonical path resolution fails (results in HTTP 400)
 */
private void prepareLogResponse(HttpGetCommand httpGetCommand, String logPath, String logName) {
    // ... implementation
}

Rationale: Security-critical code must include detailed documentation to ensure long-term maintainability


Issue 3: Security audit logs not detailed enough

Location:

  • RestHttpGetCommandProcessor.java:332
  • LogBaseServlet.java:54

Related context:

  • Log statement: logger.warning(String.format("Illegal log file path detected: %s", logName))

Problem description:
When a path traversal attack is detected, the log only records logName, not:

  • Client IP address
  • Complete request URI
  • Constructed complete file path
  • Normalized path

Potential risks:

  • Difficult to trace attack sources
  • Difficult to analyze attack patterns
  • Unable to perform security incident traceability

Impact scope:

  • Direct impact: Security monitoring and incident response
  • Indirect impact: Compliance requirements (such as SOC 2, ISO 27001)

Severity: MINOR

Improvement suggestions:

// RestHttpGetCommandProcessor.java
if (!canonicalFilePath.startsWith(canonicalLogDir + File.separator)
        && !canonicalFilePath.equals(canonicalLogDir)) {
    // Enhanced audit logging
    String clientAddress = httpGetCommand.getSocketAddress().toString();
    logger.warning(String.format(
        "Path traversal attempt blocked - Client: %s, Requested path: %s, Resolved path: %s, Log directory: %s",
        clientAddress, logName, canonicalFilePath, canonicalLogDir));
    httpGetCommand.send400();
    return;
}

// LogBaseServlet.java
if (!canonicalFilePath.startsWith(canonicalLogDir + File.separator)
        && !canonicalFilePath.equals(canonicalLogDir)) {
    String clientAddress = req.getRemoteAddr();
    String requestUri = req.getRequestURI();
    log.warn("Path traversal attempt blocked - Client: {}, URI: {}, Requested: {}, Resolved: {}, LogDir: {}",
        clientAddress, requestUri, logName, canonicalFilePath, canonicalLogDir);
    resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    return;
}

Rationale: Security incidents should record complete audit trail information


Issue 4: Missing specific handling for getCanonicalPath() IOException

Location:

  • RestHttpGetCommandProcessor.java:337
  • LogBaseServlet.java:59

Related context:

  • Exception handling: } catch (SeaTunnelRuntimeException | IOException e)

Problem description:
IOException may be triggered by multiple reasons:

  1. getCanonicalPath() failure (I/O error)
  2. readFileToStr() failure (file doesn't exist or no permission)

The current implementation uniformly returns HTTP 400 and the same error message for all cases, resulting in:

  • Unable to distinguish between "path traversal attack" and "file doesn't exist"
  • Error message "Log file content is empty" is inaccurate for I/O errors

Potential risks:

  • Reduces problem diagnosis capability
  • Attackers may fingerprint the system through I/O errors

Impact scope:

  • Direct impact: Error diagnosis and log analysis
  • Indirect impact: Operational efficiency

Severity: MINOR

Improvement suggestions:

private void prepareLogResponse(HttpGetCommand httpGetCommand, String logPath, String logName) {
    String logFilePath = logPath + "/" + logName;
    try {
        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("Illegal log file path detected: %s", logName));
            return;
        }
        String logContent = FileUtils.readFileToStr(new File(canonicalFilePath).toPath());
        this.prepareResponse(httpGetCommand, logContent);
    } catch (IOException e) {
        // getCanonicalPath() failed - possibly I/O error or permission issue
        httpGetCommand.send400();
        logger.warning(String.format("Failed to resolve log file path: %s, error: %s", 
            logFilePath, e.getMessage()));
    } catch (SeaTunnelRuntimeException e) {
        // readFileToStr() failed - file not found or read error
        httpGetCommand.send400();
        logger.warning(String.format("Log file content is empty, get log path : %s", logFilePath));
    }
}

Rationale: Distinguish between different error scenarios to improve diagnosability


Issue 5: Not using File constructor's path separator concatenation

Location:

  • RestHttpGetCommandProcessor.java:325
  • LogBaseServlet.java:47

Related context:

  • Path construction: String logFilePath = logPath + "/" + logName;

Problem description:
Although File.separator is used during path validation, path concatenation still uses hardcoded /. On Windows systems, if logPath ends with \, it will produce paths similar to C:\logs\ + / + file.log.

Although the File constructor usually handles this, it's not elegant.

Potential risks:

  • May cause path resolution issues in some edge cases
  • Poor code consistency

Impact scope:

  • Direct impact: Cross-platform compatibility
  • Severity: MINOR (actual risk is low because File constructor has good fault tolerance)

Improvement suggestions:

// Solution 1: Use File constructor to automatically handle separators
String logFilePath = new File(logPath, logName).getPath();

// Solution 2: Use Paths (Java 7+)
String logFilePath = Paths.get(logPath, logName).toString();

Rationale: Improve code consistency and cross-platform robustness


Copy link
Copy Markdown
Contributor

@dybyte dybyte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall. Could you please add some regression tests for path traversal cases (e.g., "../../etc/passwd")?

@github-actions github-actions bot added the e2e label Mar 22, 2026
dybyte
dybyte previously approved these changes Mar 23, 2026
Copy link
Copy Markdown
Contributor

@dybyte dybyte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM if CI passes

server.execInContainer(
"sh",
"-c",
"curl -s -o /dev/null -w '%{http_code}' 'http://localhost:8080/log/../../etc/passwd'");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add --path-as-is here? curl may normalize /../ by default, so the traversal payload might not reach the server unchanged.

server.execInContainer(
"sh",
"-c",
"curl -s -o /dev/null -w '%{http_code}' 'http://localhost:8080/log/../../../etc/shadow'");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

server.execInContainer(
"sh",
"-c",
"curl -s -o /dev/null -w '%{http_code}' 'http://localhost:8080/logs/../../etc/passwd'");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Copy link
Copy Markdown
Contributor

@dybyte dybyte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI didn’t pass. Let’s hold off on merging.

@zhangshenghang
Copy link
Copy Markdown
Member

@davidzollo Please check the CI

@DanielLeens
Copy link
Copy Markdown

I pulled this PR locally and traced both log-file REST paths (RestHttpGetCommandProcessor and LogBaseServlet) plus the new traversal tests in JobLogIT.

The canonical-path check now validates that the final target stays under the configured log directory before any file read, and the E2E coverage exercises both /log/... and /logs/... traversal attempts. I do not see a blocking issue in the current revision from the security-fix path itself.

Copy link
Copy Markdown

@DanielLeens DanielLeens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pulled the latest HEAD locally and checked both log-serving entry points together.

The canonical-path guard is now enforced in both RestHttpGetCommandProcessor.prepareLogResponse() and LogBaseServlet.prepareLogResponse(), so /log/... and /logs/... can no longer escape the configured log directory, and the new E2E covers the traversal cases. I do not see a blocking issue in the current revision.

@dybyte
Copy link
Copy Markdown
Contributor

dybyte commented Apr 14, 2026

@davidzollo Could you please sync this branch with the latest dev branch? It looks like the CI failed due to flaky tests.

@davidzollo davidzollo force-pushed the fix/log-path-traversal branch from 352579d to 182bfb0 Compare April 18, 2026 14:46
github-actions bot and others added 4 commits April 19, 2026 21:09
…points

Add canonical path validation to prepareLogResponse() in both
RestHttpGetCommandProcessor (legacy Hazelcast path) and LogBaseServlet
(Jetty servlet path) to prevent directory traversal attacks via the
/hazelcast/rest/maps/log/<path> and /hazelcast/rest/maps/logs/<path>
endpoints.

The fix resolves the constructed file path to its canonical form and
verifies it remains within the configured log directory boundary.
Requests attempting to escape the log directory (e.g. ../../etc/passwd)
are rejected with HTTP 400.
- Use File constructor for cross-platform path concatenation
- Separate IOException and SeaTunnelRuntimeException catch blocks for better diagnostics
- Enhance security audit logs with resolved path and log directory info
- Add path traversal regression test covering /log and /logs endpoints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@davidzollo davidzollo force-pushed the fix/log-path-traversal branch from b50d897 to cb53259 Compare April 19, 2026 13:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants