Skip to content

Commit 69c1d98

Browse files
authored
fix(site): sanitize login redirect (#15208)
1 parent 7efdf81 commit 69c1d98

File tree

2 files changed

+30
-37
lines changed

2 files changed

+30
-37
lines changed

site/src/pages/LoginPage/LoginPage.tsx

+28-35
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ export const LoginPage: FC = () => {
2828
const navigate = useNavigate();
2929
const { metadata } = useEmbeddedMetadata();
3030
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
31+
let redirectError: Error | null = null;
32+
let redirectUrl: URL | null = null;
33+
try {
34+
redirectUrl = new URL(redirectTo);
35+
} catch {
36+
// Do nothing
37+
}
38+
39+
const isApiRouteRedirect = redirectTo.startsWith("/api/v2");
3140

3241
useEffect(() => {
3342
if (!buildInfoQuery.data || isSignedIn) {
@@ -42,41 +51,24 @@ export const LoginPage: FC = () => {
4251
}, [isSignedIn, buildInfoQuery.data, user?.id]);
4352

4453
if (isSignedIn) {
45-
if (buildInfoQuery.data) {
46-
// This uses `navigator.sendBeacon`, so window.href
47-
// will not stop the request from being sent!
48-
sendDeploymentEvent(buildInfoQuery.data, {
49-
type: "deployment_login",
50-
user_id: user?.id,
51-
});
54+
// The reason we need `window.location.href` for api redirects is that
55+
// we need the page to reload and make a request to the backend. If we
56+
// use `<Navigate>`, react would handle the redirect itself and never
57+
// request the page from the backend.
58+
if (isApiRouteRedirect) {
59+
const sanitizedUrl = new URL(redirectTo, window.location.origin);
60+
window.location.href = sanitizedUrl.pathname + sanitizedUrl.search;
61+
// Setting the href should immediately request a new page. Show an
62+
// error state if it doesn't.
63+
redirectError = new Error("unable to redirect");
64+
} else {
65+
return (
66+
<Navigate
67+
to={redirectUrl ? redirectUrl.pathname : redirectTo}
68+
replace
69+
/>
70+
);
5271
}
53-
54-
// If the redirect is going to a workspace application, and we
55-
// are missing authentication, then we need to change the href location
56-
// to trigger a HTTP request. This allows the BE to generate the auth
57-
// cookie required. Similarly for the OAuth2 exchange as the authorization
58-
// page is served by the backend.
59-
// If no redirect is present, then ignore this branched logic.
60-
if (redirectTo !== "" && redirectTo !== "/") {
61-
try {
62-
// This catches any absolute redirects. Relative redirects
63-
// will fail the try/catch. Subdomain apps are absolute redirects.
64-
const redirectURL = new URL(redirectTo);
65-
if (redirectURL.host !== window.location.host) {
66-
window.location.href = redirectTo;
67-
return null;
68-
}
69-
} catch {
70-
// Do nothing
71-
}
72-
// Path based apps and OAuth2.
73-
if (redirectTo.includes("/apps/") || redirectTo.includes("/oauth2/")) {
74-
window.location.href = redirectTo;
75-
return null;
76-
}
77-
}
78-
79-
return <Navigate to={redirectTo} replace />;
8072
}
8173

8274
if (isConfiguringTheFirstUser) {
@@ -90,14 +82,15 @@ export const LoginPage: FC = () => {
9082
</Helmet>
9183
<LoginPageView
9284
authMethods={authMethodsQuery.data}
93-
error={signInError}
85+
error={signInError ?? redirectError}
9486
isLoading={isLoading || authMethodsQuery.isLoading}
9587
buildInfo={buildInfoQuery.data}
9688
isSigningIn={isSigningIn}
9789
onSignIn={async ({ email, password }) => {
9890
await signIn(email, password);
9991
navigate("/");
10092
}}
93+
redirectTo={redirectTo}
10194
/>
10295
</>
10396
);

site/src/pages/LoginPage/LoginPageView.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { CustomLogo } from "components/CustomLogo/CustomLogo";
55
import { Loader } from "components/Loader/Loader";
66
import { type FC, useState } from "react";
77
import { useLocation } from "react-router-dom";
8-
import { retrieveRedirect } from "utils/redirect";
98
import { SignInForm } from "./SignInForm";
109
import { TermsOfServiceLink } from "./TermsOfServiceLink";
1110

@@ -16,6 +15,7 @@ export interface LoginPageViewProps {
1615
buildInfo?: BuildInfoResponse;
1716
isSigningIn: boolean;
1817
onSignIn: (credentials: { email: string; password: string }) => void;
18+
redirectTo: string;
1919
}
2020

2121
export const LoginPageView: FC<LoginPageViewProps> = ({
@@ -25,9 +25,9 @@ export const LoginPageView: FC<LoginPageViewProps> = ({
2525
buildInfo,
2626
isSigningIn,
2727
onSignIn,
28+
redirectTo,
2829
}) => {
2930
const location = useLocation();
30-
const redirectTo = retrieveRedirect(location.search);
3131
// This allows messages to be displayed at the top of the sign in form.
3232
// Helpful for any redirects that want to inform the user of something.
3333
const message = new URLSearchParams(location.search).get("message");

0 commit comments

Comments
 (0)