← 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.
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
TL;DR
// 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
CVSS v3.1 Vector
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
Confidentiality (C)
High — arbitrary file read on Jenkins controller
Vector String
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
The Bug — Vulnerable Code
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
The Fix — After (Jenkins 2.442+)
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
}
→
github.com/jenkinsci/jenkins/commit/554f03782057c499c49bbb06575f0d28b5200edb
— Fix commit: Add ALLOW_AT_SYNTAX flag + withAtSyntax() gating
// 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.
Attack Chain — Step by Step
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.
Real-World Impact — Who's at Risk
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
Pull Request — The Fix
→
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);
Synthetic PR Diff — What PullLight Would Flag
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
Why Human Review Misses This
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.
Timeline
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
Fix & Resources
Fixed in: Jenkins 2.442+, LTS 2.426.3+, LTS 2.440.1+ (Jan 2024)
Fix: Apply ParserProperties.defaults().withAtSyntax(false) to all CmdLineParser instances
CmdLineParsercreated with default settings — @ file expansion enabled before auth check. Jenkins' CLICommand.getCmdLineParser() creates CmdLineParser with no ParserProperties customization, enabling @ file expansion by default. Args4j'sexpandAtFiles()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 viasecrets/master.keyexfiltration 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
/cliendpoint, making this accessible to unauthenticated attackers on any Jenkins instance with CLI enabled.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+.