diff --git a/server/server.go b/server/server.go index 5097ac86e60b0..65415d0e9fe43 100644 --- a/server/server.go +++ b/server/server.go @@ -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", "") } } diff --git a/server/server_test.go b/server/server_test.go index 4360c214117fd..9ebd537236244 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -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) diff --git a/ui/src/app/applications/components/application-urls.test.ts b/ui/src/app/applications/components/application-urls.test.ts new file mode 100644 index 0000000000000..c9063561d01af --- /dev/null +++ b/ui/src/app/applications/components/application-urls.test.ts @@ -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;

hi

'); + }).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'); +}); diff --git a/ui/src/app/applications/components/application-urls.tsx b/ui/src/app/applications/components/application-urls.tsx index ff743fc3d63f3..1d64bc8a43b9f 100644 --- a/ui/src/app/applications/components/application-urls.tsx +++ b/ui/src/app/applications/components/application-urls.tsx @@ -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