mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-12 22:20:33 +00:00
* fix(frontend): return SSO sign-ins to their next path The (auth) layout redirected every signed-in request to /workspace. The Gateway sets the session cookie on the same redirect that lands on /auth/callback?next=..., so the callback page never rendered and every SSO sign-in landed on the workspace instead of the page that asked for it. A signed-in visit to /login?next=... lost its target the same way. Layouts cannot read searchParams, so the layout now renders the page for a signed-in session and each page sends the user on: the callback and login to the validated next path, setup to the workspace. The login page shows a short wait message instead of the form while it redirects, and uses router.replace so Back does not return to it. * docs(changelog): reference #7467 for the SSO next-path fix * fix(auth): reject control characters in the next redirect path Browsers delete tab, LF and CR from a URL before parsing it, so a next of "/\n//evil.example" passed both validators' "//" check and then navigated to //evil.example. The login page's post-sign-in push already followed it, and the signed-in login and SSO callback redirects reach it too. validateAuthNextPath and validate_next_param now reject every C0 control character and DEL. Percent-encoded forms are left alone: URL parsers do not decode them, so they stay on the local origin. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
43 lines
1.1 KiB
TypeScript
43 lines
1.1 KiB
TypeScript
export const DEFAULT_AUTH_NEXT_PATH = "/workspace";
|
|
|
|
// The URL parser deletes tab, LF and CR before parsing, so `/\n//evil.example`
|
|
// passes the `//` check below and then navigates to `//evil.example`.
|
|
// Reject every C0 control and DEL rather than only the three it strips.
|
|
function hasControlCharacter(value: string): boolean {
|
|
for (let index = 0; index < value.length; index++) {
|
|
const code = value.charCodeAt(index);
|
|
if (code < 0x20 || code === 0x7f) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export function validateAuthNextPath(
|
|
nextPath: string | null | undefined,
|
|
): string | null {
|
|
if (!nextPath) {
|
|
return null;
|
|
}
|
|
if (!nextPath.startsWith("/")) {
|
|
return null;
|
|
}
|
|
if (nextPath.startsWith("//")) {
|
|
return null;
|
|
}
|
|
if (nextPath.includes("\\") || nextPath.includes(":")) {
|
|
return null;
|
|
}
|
|
if (hasControlCharacter(nextPath)) {
|
|
return null;
|
|
}
|
|
return nextPath;
|
|
}
|
|
|
|
export function resolveAuthNextPath(
|
|
nextPath: string | null | undefined,
|
|
fallback = DEFAULT_AUTH_NEXT_PATH,
|
|
): string {
|
|
return validateAuthNextPath(nextPath) ?? fallback;
|
|
}
|