← Back to PullLight
CVE-2024-23897 CVSS 9.8 Critical ● CISA KEV — Active Exploitation

CLI Argument Injection via args4j expandAtFiles() — CVE-2024-23897

Jenkins' CLI uses the args4j library to parse command arguments. Args4j's expandAtFiles() method replaces any CLI argument starting with @ with the contents of the file at that path — before Jenkins checks authorization. Unauthenticated attackers can read arbitrary files on the Jenkins controller.

CVSS 9.8 / 10.0
CWE CWE-88 → CWE-20
Product Jenkins Core
Published Jan 24, 2024
Advisory SECURITY-3314
Jenkins — widely deployed CI/CD server, millions of instances
Attack surface /cli endpoint on any Jenkins with CLI enabled
RCE chain file read → secrets/master.key → credentials → SSH into machine
// what happened
  • Affected: Jenkins ≤ 2.441, LTS ≤ 2.426.2 — all variants (packaged, Docker, Helm)
  • Attack vector: Args4j's expandAtFiles() method replaces @path/to/file with file contents as CLI args. Jenkins enabled this by default with no auth check before expansion. Unauthenticated attacker sends @/etc/passwd to /cli endpoint → first few lines of passwd returned.
  • Impact: Arbitrary file read (unauthenticated). With Overall/Read permission, full file read. RCE chain: read secrets/master.key → decrypt credentials → SSH into machine. CloudSEK confirmed active exploitation.
  • Fix: Add ALLOW_AT_SYNTAX flag (default false) controlling whether @ expansion is enabled. Patch: jenkinsci/jenkins@554f03782057c499c49bbb06575f0d28b5200edb
  • Attack surface: Any Jenkins instance with CLI enabled and network-accessible /cli endpoint

Base Score
9.8 Critical
Attack Vector (AV)
Network — reachable via HTTP/HTTPS to /cli endpoint
Attack Complexity (AC)
Low — no special conditions required
Privileges Required (PR)
None — unauthenticated for first lines; Overall/Read for full files
User Interaction (UI)
None
Scope (S)
Unchanged
Confidentiality (C)
High — arbitrary file read on Jenkins controller
Integrity (I)
None
Availability (A)
None
Vector String
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

core/src/main/java/hudson/cli/CLICommand.java — Jenkins ≤ 2.441 Jenkins creates CmdLineParser with default settings — @ expansion enabled by default protected CmdLineParser getCmdLineParser() { // VULNERABLE: default settings enable @ file expansion return new CmdLineParser(this); // ← expandAtFiles enabled by default }
core/src/main/java/hudson/cli/declarative/CLIRegisterer.java — Jenkins ≤ 2.441 Same pattern for @CLIMethod-annotated commands private CmdLineParser bindMethod(List binders) { registerOptionHandlers(); // VULNERABLE: default settings enable @ file expansion CmdLineParser parser = new CmdLineParser(null); // ← @ expansion enabled // ... }
The args4j library's expandAtFiles() method — the vulnerable sink // VULNERABLE SINK — args4j library // expandAtFiles() is called by CmdLineParser.parseArguments() before auth checks // Any CLI argument starting with @ is replaced with file contents private String[] expandAtFiles(String[] args) throws CmdLineException { List result = new ArrayList<>(); for (String arg : args) { if (arg.startsWith("@")) { File file = new File(arg.substring(1)); if (!file.exists()) throw new CmdLineException(...); // ↓ file contents become CLI args — exposed before Jenkins auth result.addAll(readAllLines(file)); } else { result.add(arg); } } return result.toArray(new String[result.size()]); } // Attacker sends: connect-node @/etc/passwd // Args4j reads /etc/passwd → ["connect-node", "root:x:0:0:", "daemon:x:1:1:", ...] // Jenkins CLI processes these as legitimate arguments — file content exposed // BEFORE Jenkins checks authorization credentials
core/src/main/java/hudson/cli/CLICommand.java — Jenkins 2.442+ Jenkins 2.442+ — @ expansion gated by ALLOW_AT_SYNTAX system property @Restricted(NoExternalUse.class) public static boolean ALLOW_AT_SYNTAX = SystemProperties.getBoolean( CLICommand.class.getName() + ".allowAtSyntax" ); protected CmdLineParser getCmdLineParser() { // FIXED: @ file expansion gated behind ALLOW_AT_SYNTAX flag ParserProperties properties = ParserProperties.defaults().withAtSyntax(ALLOW_AT_SYNTAX); return new CmdLineParser(this, properties); // ← @ expansion gated by system property }
core/src/main/java/hudson/cli/declarative/CLIRegisterer.java — Jenkins 2.442+ Jenkins 2.442+ — same fix for declarative CLI commands private CmdLineParser bindMethod(List binders) { registerOptionHandlers(); // FIXED: @ file expansion gated behind ALLOW_AT_SYNTAX flag ParserProperties properties = ParserProperties.defaults().withAtSyntax(CLICommand.ALLOW_AT_SYNTAX); CmdLineParser parser = new CmdLineParser(null, properties); // ← @ expansion gated }
// root cause
Jenkins CLICommand.getCmdLineParser() creates a CmdLineParser with args4j's default settings — which enables the @file expansion feature. This expansion happens in expandAtFiles(), which is called during argument parsing before Jenkins checks authorization on the /cli endpoint. The args4j feature was intentional and documented — but Jenkins exposed it on an unauthenticated network endpoint without any guard. The timing is the critical issue: auth checks happen after the expansion, not before. The fix adds a CLICommand.ALLOW_AT_SYNTAX system property that defaults to false, closing the expansion gate by default on all CmdLineParser instances.

From an unauthenticated CLI request to arbitrary file read on the Jenkins controller
1
Attacker identifies a Jenkins instance with CLI enabled — any Jenkins instance exposes a /cli HTTP endpoint at the same host as the web UI.
2
Attacker sends a CLI command with @/path/to/file as an argument — e.g., connect-node @/etc/passwd. Args4j's expandAtFiles() intercepts the @/etc/passwd argument.
3
expandAtFiles() reads the file at /etc/passwd and replaces @/etc/passwd with the file's contents as new CLI arguments — ["root:x:0:0:", "daemon:x:1:1:", ...]. This happens before Jenkins checks any credentials.
4
Jenkins receives and processes the expanded arguments. The file contents are returned to the attacker as part of the CLI command output — unauthenticated arbitrary file read.
5
With Overall/Read permission, attacker reads the full file (not just first lines). Reads secrets/master.key → decrypts credentials stored in Jenkins → SSH into the Jenkins controller machine.
6
Full server compromise. Jenkins controllers often run build agents with elevated privileges — the attacker has a foothold across the CI/CD infrastructure.
Any Jenkins instance with CLI enabled is exposed
  • Self-hosted Jenkins — any organization running Jenkins on-premises with CLI enabled is vulnerable if their /cli endpoint is network-accessible (including via the web UI port)
  • Docker/Helm deployments — Jenkins Docker images and Helm charts ship with CLI enabled by default, making containerized deployments a common attack surface
  • Cloud-hosted Jenkins — Jenkins instances hosted in cloud environments where the /cli endpoint is publicly accessible via HTTP are exploitable without any credentials
  • CI/CD pipelines — Jenkins controllers that orchestrate build pipelines often have broad system access; compromising the controller means compromising every build artifact and secret in the pipeline
  • Args4j usage in other apps — the pattern of new CmdLineParser(this) without withAtSyntax(false) is not unique to Jenkins; PullLight flags this pattern across all Java codebases

github.com/jenkinsci/jenkins/commit/554f03782057c499c49bbb06575f0d28b5200edb — 3 files changed, ~18 insertions, 2 deletions
core/src/main/java/hudson/cli/CLICommand.java — diff (simplified) - return new CmdLineParser(this); + ParserProperties properties = ParserProperties.defaults().withAtSyntax(ALLOW_AT_SYNTAX); + return new CmdLineParser(this, properties);
core/src/main/java/hudson/cli/declarative/CLIRegisterer.java — diff (simplified) - CmdLineParser parser = new CmdLineParser(null); + ParserProperties properties = ParserProperties.defaults().withAtSyntax(CLICommand.ALLOW_AT_SYNTAX); + CmdLineParser parser = new CmdLineParser(null, properties);
src/main/java/com/example/JenkinsRunner.java — Jenkins CLI runner with arg expansion // VULNERABLE: Jenkins CLI runner with args4j arg expansion public void runCommand(String target, String... args) { ProcessBuilder pb = new ProcessBuilder(); pb.command("java", "-jar", "jenkins-cli.jar", "-s", target); // args passed directly to CLI — @ expansion happens server-side for (String arg : args) { pb.command().add(arg); // attacker-controlled @ expansion via CLI } Process p = pb.start(); // ... } // PullLight would flag: new CmdLineParser(this) without withAtSyntax(false) // Key detail: the @ file expansion feature is a dangerous default in args4j // Jenkins enables it without auth checks on /cli endpoint
PullLight finding // PullLight flag — args4j CmdLineParser without @-syntax gating [CRITICAL] CLI Argument Injection via args4j expandAtFiles() — CLICommand.java:getCmdLineParser() CmdLineParser created with default settings — @ file expansion enabled without auth check. Any CLI argument starting with @ is replaced with file contents before authorization. CVSS 9.8 — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N Fix: Apply ParserProperties.defaults().withAtSyntax(false) to CmdLineParser
🔴 PullLight — Critical Finding
[CRITICAL] CLI Argument Injection via args4j expandAtFiles() — CLICommand.java:getCmdLineParser()

CmdLineParser created with default settings — @ file expansion enabled before auth check. Jenkins' CLICommand.getCmdLineParser() creates CmdLineParser with no ParserProperties customization, enabling @ file expansion by default. Args4j's expandAtFiles() method reads any file at the path following @ before Jenkins checks authorization. Unauthenticated attackers can read the first few lines of any file on the Jenkins controller; authenticated users with Overall/Read can read full files. CISA KEV confirmed active exploitation. RCE chain via secrets/master.key exfiltration documented by CloudSEK.

This is CWE-88 (Argument Injection)CWE-20 (Improper Input Validation) with a CVSS 9.8 rating. The @ expansion happens before credential checks on the /cli endpoint, making this accessible to unauthenticated attackers on any Jenkins instance with CLI enabled.
→ Fix: Apply ParserProperties.defaults().withAtSyntax(false) to all CmdLineParser instances. Ensure CLICommand.ALLOW_AT_SYNTAX is false by default. Upgrade Jenkins to 2.442+, LTS 2.426.3+, or LTS 2.440.1+.

Four reasons this vulnerability hides from line-by-line review

  • Args4j's @ expansion looks like a feature. The expandAtFiles() method is intentionally designed — it's documented args4j behavior. Reviewers see new CmdLineParser(this) as normal library usage, not a security risk.
  • The vulnerability is in a dependency, not app code. Jenkins uses args4j as a transitive dependency. App-level reviewers trust library defaults and focus on their own code paths.
  • Authorization check exists — but timing is wrong. Jenkins DOES check credentials — but the @ expansion happens in parseArgument() BEFORE authorization in the request lifecycle. This is a subtle timing issue that code review misses without understanding args4j's internal order of operations.
  • CLI parsing is invisible in PR diffs. When Jenkins added getCmdLineParser(), the diff showed only return new CmdLineParser(this) — a one-liner that looks innocuous. The args4j default behavior (expandAtFiles enabled) is invisible without reading args4j source.

What makes this a PullLight-specialized catch

  • Dependency vulnerability modeling: PullLight models known dangerous defaults in common libraries (args4j, commons-fileupload, etc.) and flags their usage in PR diffs.
  • Pre-auth data flow analysis: PullLight traces where data enters before auth checks — the @ expansion in parseArgument() is a pre-auth sink that most tools miss.
  • CVSS 9.8 severity calibration: PullLight correctly assigns Critical severity based on the combination of network-accessible + unauthenticated + arbitrary file read.
  • Fix precision: PullLight identifies the exact fix (ParserProperties.withAtSyntax(false)) rather than vague "disable CLI" advice.

Nov 2023 SonarSource discovers CVE-2024-23897, reports to Jenkins security team.
Jan 24, 2024 Jenkins publishes SECURITY-3314 advisory. Fix committed (554f037). Jenkins 2.442, LTS 2.426.3, LTS 2.440.1 released.
Jan 24, 2024 CVE-2024-23897 published on NVD. CVSS 9.8 confirmed.
Jan 29, 2024 SonarSource publishes detailed technical blog post.
Feb 2024 Multiple public POCs released on GitHub.
2024 CISA adds CVE-2024-23897 to Known Exploited Vulnerabilities Catalog. CloudSEK confirms active exploitation and RCE chain in the wild.
Sep 9, 2024 CISA KEV remediation deadline.
Jun 2026 PullLight documents CVE-2024-23897 as 15th CVE case study — demonstrating args4j expandAtFiles() detection in PR review.

Don't let CLI argument expansion reach production unchecked

More CVE Case Studies