mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-21 23:55:23 +00:00
* 📦 chore: Upgrade react-router-dom to v7.18.2 (security) Fixes GHSA-wrjc-x8rr-h8h6 (open redirect via backslash in Link/useNavigate, CVE-2025-68470 bypass) and GHSA-337j-9hxr-rhxg (deserializeErrors constructor injection). Neither has a 6.x patch; v7's react-router-dom is a shim re-exporting react-router, so all existing imports work unchanged. - vite manualChunks: match react-router so the routing chunk still captures the router (v7 moves all code out of the react-router-dom package) - jest: add test/polyfills.js (TextEncoder/TextDecoder + minimal Request); v7's CJS bundle constructs TextEncoder at module scope and builds a Request per navigation, neither exists in jsdom - auth specs: v7 types drop the synthetic default export; use a namespace import and mark the mock factory __esModule so the useOutletContext spy patches the object components actually read - isSafeRedirect: reject backslashes as defense in depth for the same open-redirect class the router patch addresses * 📦 chore: Regenerate stale bun.lock bun.lock predated months of package.json drift and still pinned react-router 6.30.3. Regenerated with bun install --lockfile-only so bun installs match current manifests, including react-router 7.18.2. * 🗂️ fix: Commit project-chip URL updates synchronously under router v7 v7 wraps router state updates in React.startTransition unconditionally, so the chip's paired updates tear: the conversation draft (Recoil) commits synchronously while the ?projectId removal defers. ChatRoute's draftProjectMismatch re-init sees draft != URL in that window and restores the removed project. The flushSync navigate option commits both in one pass, matching v6 ordering. Caught by the projects e2e specs. * 🧹 chore: Drop unused banner-query spy variable in Registration spec Pre-existing warning, but the changed-files eslint gate runs with --max-warnings=0 so it blocks this PR. The spy call stays; only the never-read variable goes.
41 lines
1.3 KiB
TypeScript
41 lines
1.3 KiB
TypeScript
export const REDIRECT_PARAM = 'redirect_to';
|
|
export const SESSION_KEY = 'post_login_redirect_to';
|
|
|
|
/** Matches `/login` as a full path segment, with optional basename prefix (e.g. `/librechat/login/2fa`) */
|
|
const LOGIN_PATH_RE = /(?:^|\/)login(?:\/|$)/;
|
|
|
|
/** Validates that a redirect target is a safe relative path (not an absolute or protocol-relative URL) */
|
|
export function isSafeRedirect(url: string): boolean {
|
|
if (!url.startsWith('/') || url.startsWith('//') || url.includes('\\')) {
|
|
return false;
|
|
}
|
|
const path = url.split('?')[0].split('#')[0];
|
|
return !LOGIN_PATH_RE.test(path);
|
|
}
|
|
|
|
/**
|
|
* Resolves the post-login redirect from URL params and sessionStorage,
|
|
* cleans up both sources, and returns the validated target (or null).
|
|
*/
|
|
export function getPostLoginRedirect(searchParams: URLSearchParams): string | null {
|
|
const urlRedirect = searchParams.get(REDIRECT_PARAM);
|
|
const storedRedirect = sessionStorage.getItem(SESSION_KEY);
|
|
|
|
const target = urlRedirect ?? storedRedirect;
|
|
|
|
if (storedRedirect) {
|
|
sessionStorage.removeItem(SESSION_KEY);
|
|
}
|
|
|
|
if (target == null || !isSafeRedirect(target)) {
|
|
return null;
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
export function persistRedirectToSession(value: string): void {
|
|
if (isSafeRedirect(value)) {
|
|
sessionStorage.setItem(SESSION_KEY, value);
|
|
}
|
|
}
|