Jan 21, 2026·6 min read·24 visits
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.
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 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 an href attribute 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.
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.
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:
pwned"><img src=x onerror=alert(document.cookie)>.txt.touch (or equivalent) to create the file, and the Argo Executor pushes this artifact to S3.<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.
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:
This is a textbook example of how a low-privileged input (a filename) can lead to high-privileged execution.
The remediation strategy deployed by the Argo team was robust. They didn't just fix the code; they changed the environment rules.
html/template ensures that characters like < and " are converted to safe HTML entities (<, ")../ prevents protocol handler attacks (like javascript:).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.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N| 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 |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')