Merge pull request #20 from aaronjmars/security/open-redirect-guard

fix(security): block open redirect in login redirect target
This commit is contained in:
kunkun
2026-06-01 11:17:39 +08:00
committed by GitHub
3 changed files with 69 additions and 6 deletions
+13 -3
View File
@@ -15,6 +15,16 @@ type LoginFormValues = {
confirmPassword?: string;
};
// 仅放行站内相对路径,拦截开放重定向。浏览器会忽略 URL 中的 Tab/换行/回车,并把
// //host 或 /\host 解析为协议相对的跨站地址,因此先剥离控制字符,再拒绝 // 与 /\ 前缀。
function safeRedirect(value: string | null): string {
const cleaned = (value ?? "").replace(/[\t\n\r]/g, "");
if (!cleaned.startsWith("/") || cleaned.startsWith("//") || cleaned.startsWith("/\\")) {
return "/";
}
return cleaned;
}
export default function LoginPage() {
return (
<Suspense fallback={null}>
@@ -34,7 +44,7 @@ function LoginContent() {
const linuxDoEnabled = useConfigStore((state) => state.publicSettings?.auth?.linuxDo?.enabled === true);
const allowRegister = useConfigStore((state) => state.publicSettings?.auth?.allowRegister !== false);
const [mode, setMode] = useState<"login" | "register">("login");
const redirect = searchParams.get("redirect") || "/";
const redirect = safeRedirect(searchParams.get("redirect"));
useEffect(() => {
const token = searchParams.get("token");
@@ -44,7 +54,7 @@ function LoginContent() {
void fetchCurrentUser(token).then((user) => {
setSession(token, user);
message.success("登录成功");
router.replace(redirect.startsWith("/") ? redirect : "/");
router.replace(redirect);
router.refresh();
});
}, [message, redirect, router, searchParams, setSession]);
@@ -66,7 +76,7 @@ function LoginContent() {
const action = mode === "register" ? register : login;
const user = await action({ username: values.username, password: values.password });
message.success(mode === "register" ? "注册成功" : "登录成功");
router.replace(redirect.startsWith("/") ? redirect : "/");
router.replace(redirect);
router.refresh();
if (user.role !== "admin") router.replace("/");
} catch (error) {