mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-04 17:56:38 +00:00
improve clean redirects
This commit is contained in:
@@ -66,6 +66,7 @@ export default async function Page(props: {
|
|||||||
let redirectUrl: string | undefined = undefined;
|
let redirectUrl: string | undefined = undefined;
|
||||||
if (searchParams.redirect) {
|
if (searchParams.redirect) {
|
||||||
redirectUrl = cleanRedirect(searchParams.redirect as string);
|
redirectUrl = cleanRedirect(searchParams.redirect as string);
|
||||||
|
searchParams.redirect = redirectUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
let loginIdps: LoginFormIDP[] = [];
|
let loginIdps: LoginFormIDP[] = [];
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
useSearchParams
|
useSearchParams
|
||||||
} from "next/navigation";
|
} from "next/navigation";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
|
|
||||||
export type LoginFormIDP = {
|
export type LoginFormIDP = {
|
||||||
idpId: number;
|
idpId: number;
|
||||||
@@ -63,9 +64,10 @@ export default function IdpLoginButtons({
|
|||||||
redirect || "/",
|
redirect || "/",
|
||||||
orgId
|
orgId
|
||||||
);
|
);
|
||||||
|
const safeRedirect = cleanRedirect(redirect || "/");
|
||||||
const response = await generateOidcUrlProxy(
|
const response = await generateOidcUrlProxy(
|
||||||
idpId,
|
idpId,
|
||||||
redirect || "/",
|
safeRedirect,
|
||||||
orgId
|
orgId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,86 @@
|
|||||||
type PatternConfig = {
|
type CleanRedirectOptions = {
|
||||||
name: string;
|
fallback?: string;
|
||||||
regex: RegExp;
|
maxRedirectDepth?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const patterns: PatternConfig[] = [
|
const ALLOWED_QUERY_PARAMS = new Set([
|
||||||
{ name: "Invite Token", regex: /^\/invite\?token=[a-zA-Z0-9-]+$/ },
|
"forceLogin",
|
||||||
{ name: "Setup", regex: /^\/setup$/ },
|
"code",
|
||||||
{ name: "Resource Auth Portal", regex: /^\/auth\/resource\/\d+$/ },
|
"token",
|
||||||
{
|
"redirect"
|
||||||
name: "Device Login",
|
]);
|
||||||
regex: /^\/auth\/login\/device(\?code=[a-zA-Z0-9-]+)?$/
|
|
||||||
}
|
const DUMMY_BASE = "https://internal.local";
|
||||||
];
|
|
||||||
|
export function cleanRedirect(
|
||||||
|
input: string,
|
||||||
|
options: CleanRedirectOptions = {}
|
||||||
|
): string {
|
||||||
|
const { fallback = "/", maxRedirectDepth = 2 } = options;
|
||||||
|
|
||||||
export function cleanRedirect(input: string, fallback?: string): string {
|
|
||||||
if (!input || typeof input !== "string") {
|
if (!input || typeof input !== "string") {
|
||||||
return "/";
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return sanitizeUrl(input, fallback, maxRedirectDepth);
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
}
|
}
|
||||||
const isAccepted = patterns.some((pattern) => pattern.regex.test(input));
|
}
|
||||||
return isAccepted ? input : fallback || "/";
|
|
||||||
|
function sanitizeUrl(
|
||||||
|
input: string,
|
||||||
|
fallback: string,
|
||||||
|
remainingRedirectDepth: number
|
||||||
|
): string {
|
||||||
|
if (
|
||||||
|
input.startsWith("javascript:") ||
|
||||||
|
input.startsWith("data:") ||
|
||||||
|
input.startsWith("//")
|
||||||
|
) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(input, DUMMY_BASE);
|
||||||
|
|
||||||
|
// Must be a relative/internal path
|
||||||
|
if (url.origin !== DUMMY_BASE) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url.pathname.startsWith("/")) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanParams = new URLSearchParams();
|
||||||
|
|
||||||
|
for (const [key, value] of url.searchParams.entries()) {
|
||||||
|
if (!ALLOWED_QUERY_PARAMS.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "redirect") {
|
||||||
|
if (remainingRedirectDepth <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedRedirect = sanitizeUrl(
|
||||||
|
value,
|
||||||
|
"",
|
||||||
|
remainingRedirectDepth - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cleanedRedirect) {
|
||||||
|
cleanParams.set("redirect", cleanedRedirect);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanParams.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = cleanParams.toString();
|
||||||
|
return queryString ? `${url.pathname}?${queryString}` : url.pathname;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user