Feb 26, 2026·5 min read·66 visits
Argo CD trusted user input when rendering repository URLs. Attackers with 'repo edit' permissions can inject `javascript:` payloads. When an admin views the settings page, the payload executes, allowing the attacker to ride the admin's session to delete apps, steal secrets, or deploy malware.
A critical Stored Cross-Site Scripting (XSS) vulnerability has been discovered in Argo CD, the industry-standard GitOps continuous delivery tool. By injecting a malicious URL protocol into the repository configuration, an attacker can execute arbitrary JavaScript in the browser of any user viewing the repository list—typically a high-privileged administrator. This effectively bridges the gap between low-level configuration access and full Kubernetes cluster compromise.
Argo CD is the darling of the Cloud Native world. It’s the bouncer, the gatekeeper, and the janitor for your Kubernetes clusters. It holds the keys to the castle—literally—managing secrets, deployments, and infrastructure state. If you compromise Argo CD, you don't just get a shell; you get kubectl apply -f ownage.yaml on every cluster it manages.
Usually, we think of XSS as a "medium" severity annoyance—alert boxes and maybe some cookie theft. But context is king. In an administrative dashboard like Argo CD, XSS is the digital equivalent of a Jedi mind trick on the sysadmin. You aren't hacking the server; you are convincing the server that the admin wants to delete the production database. This vulnerability, CVE-2025-47933, turns a simple UI bug into a catastrophic infrastructure collapse.
The road to hell is paved with helpful utility libraries. In this case, the vulnerability lies in how Argo CD's frontend handles Git URLs. To make the UI look pretty, the developers used a library called git-url-parse to break down repository URLs into their component parts (protocol, owner, resource, etc.) and then reassemble them for display.
Here is the logic flaw: The code assumed that if a URL could be parsed, it was safe to display. It took the parsed components and blindly concatenated them back into a string to be used in an href attribute. It failed to ask the most important question in web security: "Is this protocol actually safe?"
By supplying a URL that technically satisfies the parser but utilizes the javascript: pseudo-protocol, an attacker can turn a clickable link into a stored execution trigger. It’s a classic case of sanitization happening in the wrong place—or in this case, not happening at all.
Let's look at the crime scene in ui/src/app/shared/components/urls.ts. The vulnerable function repoUrl takes a string and tries to return a formatted URL.
The Vulnerable Code:
export function repoUrl(url: string): string {
try {
const parsed = GitUrlParse(url);
// The fatal flaw: Blind concatenation without validation
return `${protocol(parsed.protocol)}://${parsed.resource}/${parsed.owner}/${parsed.name}`;
} catch {
return null;
}
}See that return statement? It constructs a string using the protocol derived from the input. If I pass in javascript:alert(1), and git-url-parse is lenient enough to treat javascript as the protocol, the function happily returns a valid JS URI.
The Fix:
The patch is simple: validation. The maintainers introduced an isValidURL check that likely whitelists protocols like http, https, ssh, and git.
import {isValidURL} from '../../shared/utils';
export function repoUrl(url: string): string {
try {
const parsed = GitUrlParse(url);
const parsedUrl = `${protocol(parsed.protocol)}://${parsed.resource}/${parsed.owner}/${parsed.name}`;
// The Guard Rail
if (!isValidURL(parsedUrl)) {
return null;
}
return parsedUrl;
}
}It’s a three-line fix for a 9.1 CVSS vulnerability. Security is often boring like that.
So, how do we burn it down? We need two things: access to create/edit a repository, and an admin victim.
Step 1: The Setup
We authenticate as a low-privileged user (perhaps a developer with access to a specific project). We navigate to Settings -> Repositories or use the CLI to register a new repo. Instead of a valid GitHub URL, we provide our payload. The parser might be tricky, so we craft it to look like a valid Git URL structure but with a malicious protocol.
Step 2: The Payload
We inject something nasty. We don't want an alert box; we want persistence.
javascript:fetch('/api/v1/account/password', {method: 'PUT', body: JSON.stringify({currentPassword: '...', newPassword: 'owned'})})Or perhaps we simply delete the production application:
javascript:fetch('/api/v1/applications/prod-app?cascade=true', {method: 'DELETE'})Step 3: The Trap
The URL is saved to Kubernetes secrets. Now we wait. An Argo CD administrator logs in to check why a sync failed or to audit the repositories. They navigate to the Repositories page. The UI renders our malicious link. Depending on the browser behavior and the exact rendering, this might trigger on a stray click, or we could style the link to cover the whole screen using CSS injection if the inputs allow it.
Step 4: Game Over
The script executes in the context of the Admin. The browser sends the API request with the Admin's cookies/tokens. The API sees a valid request from a Super Admin. The cluster is yours.
If you are running a vulnerable version, you are one disgruntled employee or one compromised dev account away from total disaster.
Because Argo CD is declarative, this XSS allows an attacker to rewrite the "truth" of your infrastructure. They could inject a sidecar container into your payment processing pods to skim credit cards. They could modify the OIDC configuration to lock everyone out. They could simply wipe the cluster.
This isn't just a UI bug; it's a privilege escalation tunnel from "I can edit git repos" to "I am Root on the Cluster".
Don't try to get clever with WAF rules here; the payload is stored encrypted in your secrets and rendered by the client. You need to patch the binary.
Update Immediately:
If you cannot patch immediately, you must restrict RBAC permissions. Remove repositories, update and repositories, create from all non-admin roles. And for the love of Ops, audit your current repositories:
kubectl get secrets -n argocd -l argocd.argoproj.io/secret-type=repository -o jsonpath="{.items[*].data.url}" | base64 -dIf you see javascript: in there, pull the fire alarm.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Argo CD Argo Project | >= 1.2.0-rc1, < 2.13.8 | 2.13.8 |
Argo CD Argo Project | >= 2.14.0-rc1, < 2.14.13 | 2.14.13 |
Argo CD Argo Project | >= 3.0.0-rc1, < 3.0.4 | 3.0.4 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 (Cross-Site Scripting) |
| CVSS v3.1 | 9.1 (Critical) |
| Attack Vector | Network (Stored in Config) |
| Privileges Required | Low (Repo Edit) |
| Impact | Remote Code Execution (via API) |
| Patch Status | Available |