fix: prevent open redirect in OIDC callback flow (#61)

This commit is contained in:
Copilot
2026-02-23 15:31:02 +08:00
committed by GitHub
parent 7898a2703b
commit 3b632beee4
3 changed files with 35 additions and 2 deletions
+2 -1
View File
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"
import { useAuth } from "@/contexts/auth-context"
import { useMessage } from "@/lib/feedback/message"
import { parseOidcCallback } from "@/lib/oidc"
import { isSafeRedirectPath } from "@/lib/routes"
import { useTranslation } from "react-i18next"
export default function OidcCallbackPage() {
@@ -30,7 +31,7 @@ export default function OidcCallbackPage() {
return
}
redirectPath.current = credentials.redirect || "/browser"
redirectPath.current = isSafeRedirectPath(credentials.redirect, "/browser")
loginWithStsCredentials({
AccessKeyId: credentials.accessKey,
+2 -1
View File
@@ -1,4 +1,5 @@
import type { OidcProvider } from "@/types/config"
import { isSafeRedirectPath } from "@/lib/routes"
/**
* Fetch configured OIDC providers from the server.
@@ -52,6 +53,6 @@ export function parseOidcCallback(hash: string): {
secretKey,
sessionToken,
expiration: params.get("expiration") ?? "",
redirect: params.get("redirect") ?? "/",
redirect: isSafeRedirectPath(params.get("redirect") ?? "", "/"),
}
}
+31
View File
@@ -26,3 +26,34 @@ export function buildRoute(path: string): string {
export function getLoginRoute(): string {
return buildRoute("/auth/login")
}
/**
* Validate that a redirect path is safe (relative, no protocol, no external domain).
* Returns the path if safe, or the fallback otherwise.
*/
export function isSafeRedirectPath(path: string, fallback = "/"): string {
if (!path || typeof path !== "string") return fallback
const trimmed = path.trim()
// Block absolute URLs with protocol (e.g. https://evil.com)
if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) return fallback
// Block protocol-relative URLs and backslash variants (e.g. //evil.com, \/evil.com, \\evil.com)
// Some browsers treat backslashes as forward slashes in URLs
if (/^[\\/]{2,}/.test(trimmed)) return fallback
// Block single leading backslash (e.g. \evil.com) which some browsers interpret as //
if (trimmed.startsWith("\\")) return fallback
// Block data: and javascript: URIs (case-insensitive, with optional whitespace)
if (/^\s*(javascript|data)\s*:/i.test(trimmed)) return fallback
// Ensure it starts with / (relative path)
if (!trimmed.startsWith("/")) return fallback
// Block directory traversal sequences (e.g. /../evil.com, /../../etc/passwd)
if (/(?:^|\/)\.\.(?:\/|$)/.test(trimmed)) return fallback
return trimmed
}