Argo Workflows: The Artifact Directory Trap
Jan 21, 2026·6 min read·7 visits
Executive Summary (TL;DR)
Argo Workflows didn't sanitize filenames in its artifact browser. An attacker can create a file named `<script>alert(1)</script>`, and when an admin views the file list in the UI, the script executes. This leads to session hijacking and potential Kubernetes cluster compromise via the Argo API.
A high-severity Stored Cross-Site Scripting (XSS) vulnerability exists in the Argo Workflows Artifact Server. By crafting malicious filenames in workflow outputs, attackers can inject arbitrary JavaScript that executes when an administrator views the artifact directory listing.
The Hook: Trusting the File System
In the world of Kubernetes, Argo Workflows is the heavy lifter. It orchestrates complex jobs, moving data from point A to point B. Part of that job involves 'artifacts'—files generated by one step and stored for later use or inspection. We implicitly trust these files. We assume that a file system is just a dumb bucket of bytes and names.
But here is the thing: the web doesn't care about your file system's neutrality. To the Argo Server, a filename isn't just metadata; it's a string that needs to be rendered into an HTML page so a developer can click it. And that is where the assumption of safety dies a painful death.
CVE-2026-23960 isn't some complex memory corruption in the Go runtime. It is a classic, almost nostalgic, failure to sanitize user input before dumping it into the DOM. It turns the mundane act of browsing a directory listing into a game of Russian Roulette with your cluster admin credentials.
The Flaw: Concatenation Catastrophe
The vulnerability lies deep within server/artifacts/artifact_server.go. The developers needed to build a simple directory listing for artifacts stored in S3 or GCS. In a rush to get bytes to the browser, they committed the cardinal sin of web development: Manual String Concatenation.
Instead of using a context-aware templating engine, the code looped through the object keys and effectively said: 'Take this string and shove it into this HTML tag.' Specifically, they used fmt.Fprintf to build list items.
[!WARNING] The Logic Error The code took
file(a string derived directly from the object name in storage) and injected it directly into anhrefattribute and the anchor text without escaping.
Because the filename is controlled by the workflow (and thus the attacker), I can name my output file whatever I want. If I name it "><script>..., the code blindly closes the quote, closes the tag, and opens a new portal to hell right inside your browser.
The Code: The Smoking Gun
Let's look at the crime scene. This is the code running inside the Argo Artifact Server prior to the patch:
// VULNERABLE CODE
// server/artifacts/artifact_server.go
for _, object := range objects {
dir, file := path.Split(strings.TrimPrefix(object, key+"/"))
if dir == "" {
// Direct injection of 'file' variable into HTML
_, _ = fmt.Fprintf(w, "<li><a href=\"%s\">%s</a></li>\n", file, file)
} else {
_, _ = fmt.Fprintf(w, "<li><a href=\"%s\">%s</a></li>\n", dir, dir)
}
}It is brutally simple. If file contains ", it breaks the href attribute. If it contains <, it starts a new tag.
The fix introduced in commit 159a5c5 replaces this reckless printing with Go's html/template package, which is smart enough to escape dangerous characters. But the patch authors went a step further—they didn't just escape the text; they hardened the logic:
// PATCHED CODE
// Notice the template usage and the "./" prefix
tmpl, err := template.New("list").Parse("<li><a href=\"./{{.}}\">{{.}}</a></li>\n")Why the ./ prefix? That is a subtle stroke of genius. It forces the browser to interpret the link as a relative path. Even if an attacker somehow injected javascript:alert(1) as the filename, the browser would try to navigate to ./javascript:alert(1), which is a harmless 404, effectively killing the payload.
The Exploit: Weaponizing Filenames
To exploit this, we don't need fancy tools. We just need to submit a workflow. If you have permission to run workflows in a namespace (a common privilege for developers), you can compromise the cluster administrator.
Here is the attack chain:
- Draft the Malicious Workflow: We create a workflow step that generates a file. We don't care about the file content; we care about the filename.
- The Payload: We name the file
pwned"><img src=x onerror=alert(document.cookie)>.txt. - Execution: We submit the workflow. The pod starts, runs
touch(or equivalent) to create the file, and the Argo Executor pushes this artifact to S3. - The Trap: We wait. Eventually, an admin or a senior dev will browse the workflow details to debug logs or check outputs.
- Detonation: They click the 'Artifacts' tab. The server iterates the file list, sees our malicious filename, and renders:
<li><a href="pwned"><img src=x onerror=alert(document.cookie)>.txt">...</a></li>The browser sees the closing ", thinks the anchor tag is done, and immediately processes the <img> tag. The src=x fails, the onerror fires, and your session cookies are sent to my discord webhook.
The Impact: From UI to API
Why is XSS in Argo such a big deal? It's just a dashboard, right?
Wrong. The Argo UI is a Single Page Application (SPA) that talks to the Kubernetes API and the Argo Server API using the user's browser credentials. When an XSS triggers, it runs with the permissions of the victim.
If the victim is a Cluster Admin:
- Session Hijacking: The attacker steals the JWT or auth cookie.
- API Abuse: The attacker's script can make XHR requests to the Argo API to delete workflows, change secrets, or trigger new workflows running malicious containers with privileged security contexts.
- Lateral Movement: If the dashboard is internal, the attacker has now bridged the gap from an external workflow submission to the internal management plane.
This is a textbook example of how a low-privileged input (a filename) can lead to high-privileged execution.
The Fix: Defense in Depth
The remediation strategy deployed by the Argo team was robust. They didn't just fix the code; they changed the environment rules.
- Context-Aware Escaping: Switching to
html/templateensures that characters like<and"are converted to safe HTML entities (<,"). - Path Neutralization: Prepending
./prevents protocol handler attacks (likejavascript:). - Content Security Policy (CSP): This is the hammer. They added a strict CSP header to the artifact server responses:
Content-Security-Policy: sandbox; base-uri 'none'; default-src 'none'; ...
The sandbox directive is particularly brutal. It tells the browser: "Treat this content as if it came from a unique, opaque origin." Even if the HTML escaping failed, the browser would refuse to execute the script because the sandbox prevents script execution entirely. This is how you patch vulnerabilities: not just by fixing the bug, but by making the bug impossible to exploit.
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Argo Workflows Argo Project | < 3.6.17 | 3.6.17 |
Argo Workflows Argo Project | < 3.7.8 | 3.7.8 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network |
| CVSS | 8.1 (High) |
| Exploit Status | PoC Available |
| Privileges Required | Low (Workflow Submitter) |
| User Interaction | Required |
MITRE ATT&CK Mapping
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.