Files
prowler/ui/lib/auth-callback-url.ts
T
2026-07-06 14:49:07 +02:00

66 lines
1.9 KiB
TypeScript

const DEFAULT_CALLBACK_PATH = "/";
const INVITATION_TOKEN_PARAM = "invitation_token";
// Origin used only to resolve relative paths; never part of the returned value.
const INTERNAL_ORIGIN = "http://localhost";
type CallbackSearchParams = {
get(name: string): string | null;
};
export const getSafeCallbackPathFromValue = (
value: string | null | undefined,
) => {
if (!value || !value.startsWith("/") || value.startsWith("//")) {
return DEFAULT_CALLBACK_PATH;
}
// A prefix check is not enough: the URL parser normalizes backslashes and
// control characters, so "/\evil.com" or "/\t/evil.com" pass the check above
// yet resolve to an external origin. Resolve against a fixed origin and
// confirm it stayed internal before trusting the path.
try {
const url = new URL(value, INTERNAL_ORIGIN);
if (url.origin !== INTERNAL_ORIGIN) {
return DEFAULT_CALLBACK_PATH;
}
return `${url.pathname}${url.search}${url.hash}`;
} catch (_error) {
return DEFAULT_CALLBACK_PATH;
}
};
export const getSafeCallbackPath = (
searchParams: CallbackSearchParams,
key = "state",
) => getSafeCallbackPathFromValue(searchParams.get(key));
export const appendCallbackState = (authUrl: string, callbackPath: string) => {
const safeCallbackPath = getSafeCallbackPathFromValue(callbackPath);
if (safeCallbackPath === DEFAULT_CALLBACK_PATH) {
return authUrl;
}
try {
const url = new URL(authUrl);
url.searchParams.set("state", safeCallbackPath);
return url.toString();
} catch (_error) {
return authUrl;
}
};
export const getInvitationTokenFromCallbackPath = (callbackPath: string) => {
const safeCallbackPath = getSafeCallbackPathFromValue(callbackPath);
if (safeCallbackPath === DEFAULT_CALLBACK_PATH) {
return null;
}
try {
const url = new URL(safeCallbackPath, "http://localhost");
return url.searchParams.get(INVITATION_TOKEN_PARAM);
} catch (_error) {
return null;
}
};