BUG_Author: Zy0t1

Affected Version: flow-ci / flow-core-x all versions (since 2019 to master branch, never patched)

Vendor: FlowCI/flow-core-x

Software: flow-ci

Vulnerability Files: - common/src/main/java/com/flowci/common/helper/FileHelper.java (L98-L112, unzip method)

CWE: CWE-22 - Improper Limitation of a Pathname to a Restricted Directory ("Path Traversal") - The software uses external input to construct a pathname that is intended to identify a file or directory located beneath a restricted parent directory, but it does not properly neutralize special elements such as ../ sequences that can resolve to a location that is outside of that directory.

CVSS 3.1: 7.6 (High) - AV:N/AC:H/PR:L/UI:R/S:C/C:N/I:H/A:H

Description:

flow-ci's FileHelper.unzip() extracts ZIP archives using ZipInputStream and constructs output paths with Paths.get(destDir.toString(), entry.getName()). The ZIP entry name is attacker-controlled metadata and is never sanitized. There is no normalize() call, no rejection of .., and no check that the resolved path remains inside destDir. A ZIP entry such as ../../../../cve03_browser_escape.txt is therefore written outside the extraction directory.

The production trigger chain is:

  1. Upload phase: An agent uploads a ZIP archive as a job report through POST /api/flow/{name}/job/{number}/report. The report metadata sets zipped: true.
  2. Extraction phase: When an authenticated user requests the report through GET /jobs/{flow}/{buildNumber}/reports/{reportId}, ReportServiceImpl.fetch() passes the archive stream to FileHelper.unzip(stream, destFile).
  3. Path traversal: The ZipEntry name is joined to the destination path without validation, allowing the archive entry to escape the report extraction directory.

The vulnerable method is:

// common/src/main/java/com/flowci/common/helper/FileHelper.java L98-L112
public static void unzip(InputStream src, Path destDir) throws IOException {
    try (ZipInputStream zipIn = new ZipInputStream(src)) {
        ZipEntry entry = zipIn.getNextEntry();
        while (entry != null) {
            Path filePath = Paths.get(destDir.toString(), entry.getName());
            if (!entry.isDirectory()) {
                writeToFile(zipIn, filePath);
            } else {
                filePath.toFile().mkdirs();
            }
            zipIn.closeEntry();
            entry = zipIn.getNextEntry();
        }
    }
}

Prerequisites/Setup:

  1. flow-ci is deployed via Docker and the web console is available at http://localhost:2015.
  2. An authenticated user can create a Flow and edit .flowci.yml in the browser.
  3. No online agent is required for the component-level browser PoC.

Proof of Concept (Browser-based verification):