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