mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-27 13:40:37 +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>
47 lines
1.9 KiB
TypeScript
47 lines
1.9 KiB
TypeScript
import { describe, expect, test } from "@rstest/core";
|
|
|
|
import {
|
|
resolveAuthNextPath,
|
|
validateAuthNextPath,
|
|
} from "@/core/auth/next-path";
|
|
|
|
describe("auth next path validation", () => {
|
|
test("accepts local absolute paths", () => {
|
|
expect(validateAuthNextPath("/workspace")).toBe("/workspace");
|
|
expect(validateAuthNextPath("/workspace/chats/new?tab=recent#top")).toBe(
|
|
"/workspace/chats/new?tab=recent#top",
|
|
);
|
|
});
|
|
|
|
test("rejects external and ambiguous redirects", () => {
|
|
expect(validateAuthNextPath(null)).toBeNull();
|
|
expect(validateAuthNextPath("workspace")).toBeNull();
|
|
expect(validateAuthNextPath("//evil.example")).toBeNull();
|
|
expect(validateAuthNextPath("https://evil.example")).toBeNull();
|
|
expect(validateAuthNextPath("/:evil")).toBeNull();
|
|
expect(validateAuthNextPath("/\\evil.example")).toBeNull();
|
|
expect(validateAuthNextPath("/foo\\bar")).toBeNull();
|
|
});
|
|
|
|
test("rejects control characters the URL parser would delete", () => {
|
|
// Tab, LF and CR are deleted before parsing, which turns each of these
|
|
// into a protocol-relative URL for evil.example.
|
|
expect(validateAuthNextPath("/\t//evil.example")).toBeNull();
|
|
expect(validateAuthNextPath("/\n//evil.example")).toBeNull();
|
|
expect(validateAuthNextPath("/\r//evil.example")).toBeNull();
|
|
expect(validateAuthNextPath("/\n/evil.example")).toBeNull();
|
|
expect(validateAuthNextPath("/workspace\u0000")).toBeNull();
|
|
expect(validateAuthNextPath("/workspace\u007f")).toBeNull();
|
|
// Percent-encoded forms are not decoded by the parser and stay local.
|
|
expect(validateAuthNextPath("/%0A//evil.example")).toBe(
|
|
"/%0A//evil.example",
|
|
);
|
|
});
|
|
|
|
test("falls back for unsafe paths", () => {
|
|
expect(resolveAuthNextPath("/\\evil.example")).toBe("/workspace");
|
|
expect(resolveAuthNextPath("/\n//evil.example")).toBe("/workspace");
|
|
expect(resolveAuthNextPath("/safe", "/fallback")).toBe("/safe");
|
|
});
|
|
});
|