External URLs for Deployments can include javascript in argo-cd
Description
Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. All versions of Argo CD starting with v1.0.0 are vulnerable to a cross-site scripting (XSS) bug allowing a malicious user to inject a javascript: link in the UI. When clicked by a victim user, the script will execute with the victim's permissions (up to and including admin). The script would be capable of doing anything which is possible in the UI or via the API, such as creating, modifying, and deleting Kubernetes resources. A patch for this vulnerability has been released in the following Argo CD versions: v2.4.1, v2.3.5, v2.2.10 and v2.1.16. There are no completely-safe workarounds besides upgrading.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/argoproj/argo-cdGo | >= 1.0.0, < 2.1.16 | 2.1.16 |
github.com/argoproj/argo-cd/v2Go | < 2.1.16 | 2.1.16 |
github.com/argoproj/argo-cd/v2Go | >= 2.2.0, < 2.2.10 | 2.2.10 |
github.com/argoproj/argo-cd/v2Go | >= 2.3.0, < 2.3.5 | 2.3.5 |
github.com/argoproj/argo-cd/v2Go | >= 2.4.0, < 2.4.1 | 2.4.1 |
Affected products
1Patches
18bc3ef690de2Merge pull request from GHSA-h4w9-6x78-8vrj
4 files changed · +55 −3
server/server.go+1 −0 modified@@ -1010,6 +1010,7 @@ func (a *ArgoCDServer) Authenticate(ctx context.Context) (context.Context, error if !argoCDSettings.AnonymousUserEnabled { return ctx, claimsErr } else { + // nolint:staticcheck ctx = context.WithValue(ctx, "claims", "") } }
server/server_test.go+1 −1 modified@@ -508,7 +508,7 @@ func getTestServer(t *testing.T, anonymousEnabled bool, withFakeSSO bool) (argoc cm.Data["users.anonymous.enabled"] = "true" } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - return // Start with a placeholder. We need the server URL before setting up the real handler. + // Start with a placeholder. We need the server URL before setting up the real handler. })) ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { dexMockHandler(t, ts.URL)(w, r)
ui/src/app/applications/components/application-urls.test.ts+20 −0 added@@ -0,0 +1,20 @@ +import {ExternalLink, InvalidExternalLinkError} from './application-urls'; + +test('rejects malicious URLs', () => { + expect(() => { + const _ = new ExternalLink('javascript:alert("hi")'); + }).toThrowError(InvalidExternalLinkError); + expect(() => { + const _ = new ExternalLink('data:text/html;<h1>hi</h1>'); + }).toThrowError(InvalidExternalLinkError); +}); + +test('allows absolute URLs', () => { + expect(new ExternalLink('https://localhost:8080/applications').ref).toEqual('https://localhost:8080/applications'); +}); + +test('allows relative URLs', () => { + // @ts-ignore + window.location = new URL('https://localhost:8080/applications'); + expect(new ExternalLink('/applications').ref).toEqual('/applications'); +});
ui/src/app/applications/components/application-urls.tsx+33 −2 modified@@ -1,7 +1,15 @@ import {DropDownMenu} from 'argo-ui'; import * as React from 'react'; -class ExternalLink { +export class InvalidExternalLinkError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, InvalidExternalLinkError.prototype); + this.name = 'InvalidExternalLinkError'; + } +} + +export class ExternalLink { public title: string; public ref: string; @@ -14,13 +22,36 @@ class ExternalLink { this.title = url; this.ref = url; } + if (!ExternalLink.isValidURL(this.ref)) { + throw new InvalidExternalLinkError('Invalid URL'); + } + } + + private static isValidURL(url: string): boolean { + try { + const parsedUrl = new URL(url); + return parsedUrl.protocol !== 'javascript:' && parsedUrl.protocol !== 'data:'; + } catch (TypeError) { + try { + // Try parsing as a relative URL. + const parsedUrl = new URL(url, window.location.origin); + return parsedUrl.protocol !== 'javascript:' && parsedUrl.protocol !== 'data:'; + } catch (TypeError) { + return false; + } + } } } export const ApplicationURLs = ({urls}: {urls: string[]}) => { const externalLinks: ExternalLink[] = []; for (const url of urls || []) { - externalLinks.push(new ExternalLink(url)); + try { + const externalLink = new ExternalLink(url); + externalLinks.push(externalLink); + } catch (InvalidExternalLinkError) { + continue; + } } // sorted alphabetically & links with titles first
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-h4w9-6x78-8vrjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-31035ghsaADVISORY
- argo-cd.readthedocs.io/en/stable/user-guide/external-urlghsaWEB
- argo-cd.readthedocs.io/en/stable/user-guide/external-url/mitrex_refsource_MISC
- github.com/argoproj/argo-cd/commit/8bc3ef690de29c68a36f473908774346a44d4038ghsax_refsource_MISCWEB
- github.com/argoproj/argo-cd/security/advisories/GHSA-h4w9-6x78-8vrjghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.