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: - core/src/main/java/com/flowci/core/common/manager/ConditionManagerImpl.java (L39-L72, run method)

CWE: CWE-94 - Improper Control of Generation of Code (Code Injection) - The software constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.

CVSS 3.1: 9.9 (Critical) - AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

Description:

flow-ci evaluates step-level condition fields from .flowci.yml pipeline configurations using GroovyShell without any sandbox restrictions. ConditionManagerImpl.run() creates a new GroovyShell(binding) and calls groovy.evaluate(groovyScript) directly — there is no SecureASTCustomizer, no CompilerConfiguration whitelist, and no SecurityManager fallback. The script content is entirely user-controlled through the step-level condition field.

The condition is evaluated in the flow-ci core container, inside the flowci-core.jar JVM process. It is not evaluated in an agent container or in a temporary job container created for a pipeline step. In JobActionServiceImpl.executeJob(), runCondition() is called before the code attempts to locate an agent, dispatch the bash step, or set the step to WAITING_AGENT. A condition-only browser PoC therefore succeeds even when no agent is online.

Runtime verification confirms that the executing process has PID 1, the command line java ... -jar flowci-core.jar, working directory /flow.ci, and the core container hostname. This proves that Groovy code executes in the same root process that serves the flow-ci core API, not in a job/agent environment where arbitrary commands might already be expected.

Key details:

  1. No sandbox: GroovyShell is instantiated without a SecureASTCustomizer or CompilerConfiguration, allowing arbitrary Java method calls including Runtime.exec(), ProcessBuilder, File I/O, and ClassLoader access.
  2. Step-level condition: The condition must be defined at the step level (not the root level). JobActionServiceImpl.runCondition() skips root-level conditions for API-triggered jobs (if (node.getPath().isRoot()) return true).
  3. Execution occurs before agent dispatch: executeJob() calls runCondition() before fetchAgentFromJob(), fetchAgentFromPool(), and dispatch(). No agent is required for the Groovy code to execute.
  4. Core container compromise: Because the code executes inside the core JVM as root, an authenticated user can fully compromise the CI/CD server rather than gaining code execution only inside an isolated job container.
  5. 2-second timeout is insufficient: DefaultTimeout = 2 only attempts to cancel the Future. File writes and process creation can complete before cancellation.

Prerequisites/Setup:

  1. flow-ci is deployed via Docker, with the web console exposed at http://localhost:2015 and core API at http://localhost:8080.
  2. The attacker has an authenticated account with Flow creation permissions. The verification account is [email protected] / test@flowci.