feat(tradein/ui): login-форма + session-aware RouteGuard (#2555) #2562
2 changed files with 30 additions and 3 deletions
|
|
@ -43,11 +43,31 @@ function readNextParam(): string | null {
|
|||
* Open-redirect guard: принимаем только внутренний путь, начинающийся
|
||||
* ровно с одного "/" — не "//host" (protocol-relative URL) и не "/\host"
|
||||
* (браузеры местами трактуют backslash как forward slash в URL-парсинге).
|
||||
*
|
||||
* PR #2562 review finding 1: WHATWG URL-парсер (который `router.push`
|
||||
* использует под капотом) убирает ВСЕ ASCII tab/CR/LF из строки ПЕРЕД
|
||||
* парсингом — так `"/\t//evil"` для наивного regex выглядит как безопасный
|
||||
* путь с одним leading slash (символ в позиции 1 — таб, не "/" и не "\"),
|
||||
* а после навигации превращается в `"//evil"` (protocol-relative → чужой
|
||||
* origin). Убираем те же символы ДО валидации, чтобы regex видел ту же
|
||||
* строку, что увидит парсер.
|
||||
*
|
||||
* PR #2562 review finding 2: `next=/login` (или `/login?...`) после успешного
|
||||
* логина кидал бы юзера обратно на форму входа (RouteGuard не гейтит
|
||||
* `/login`) — dead-end. Фолбэк на "/" в этом случае.
|
||||
*/
|
||||
function sanitizeNext(next: string | null): string {
|
||||
if (!next) return "/";
|
||||
if (!/^\/(?!\/|\\)/.test(next)) return "/";
|
||||
return next;
|
||||
const cleaned = next.replace(/[\t\r\n]/g, "");
|
||||
if (!/^\/(?!\/|\\)/.test(cleaned)) return "/";
|
||||
if (
|
||||
cleaned === "/login" ||
|
||||
cleaned.startsWith("/login?") ||
|
||||
cleaned.startsWith("/login#")
|
||||
) {
|
||||
return "/";
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
function loginErrorMessage(error: unknown): string {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,14 @@ export function RouteGuard({ children }: RouteGuardProps) {
|
|||
|
||||
useEffect(() => {
|
||||
if (!shouldRedirectToLogin) return;
|
||||
router.push(`/login?next=${encodeURIComponent(rawPath)}`);
|
||||
// PR #2562 review finding 3: deep-links carry их state в query (`/v2?id=
|
||||
// <uuid>` — см. next.config.ts redirect comment про restore-by-id). Без
|
||||
// `window.location.search` юзер, чья сессия истекла mid-session на такой
|
||||
// ссылке, после логина попадал бы на голый `/v2` и терял отчёт. Effect
|
||||
// — гарантированно client-side (useEffect тело никогда не бежит на SSR),
|
||||
// поэтому `window` тут безопасен без typeof-guard.
|
||||
const next = `${rawPath}${window.location.search}`;
|
||||
router.push(`/login?next=${encodeURIComponent(next)}`);
|
||||
}, [shouldRedirectToLogin, rawPath, router]);
|
||||
|
||||
// #801: preview-страница самодостаточна (свой QueryClient с фейковым me),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue