mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-27 13:40:37 +00:00
fix(api): survive a malformed CSRF cookie (#7589)
* fix(api): survive a malformed CSRF cookie `readCsrfCookie` percent-decodes a cookie the gateway sets verbatim. Cookies are shared by every app on the same host (ports included), so a stray or truncated `%` escape from another local app, a proxy, or a stale value makes `decodeURIComponent` throw `URIError` inside `fetch` and `getCsrfHeaders` — every state-changing call fails with an exception before the gateway can answer 403. Fall back to the raw cookie value when decoding fails, matching `getLocaleFromCookie` (#7553). * test(api): exercise sibling CSRF cookies in the DOM --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
ab8cd4e10f
commit
52f6ff078c
@ -39,7 +39,16 @@ export function readCsrfCookie(): string | null {
|
||||
if (typeof document === "undefined") return null;
|
||||
for (const pair of document.cookie.split("; ")) {
|
||||
if (pair.startsWith(CSRF_COOKIE_PREFIX)) {
|
||||
return decodeURIComponent(pair.slice(CSRF_COOKIE_PREFIX.length));
|
||||
const value = pair.slice(CSRF_COOKIE_PREFIX.length);
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch {
|
||||
// Cookies are shared by every app on the same host. A value set or
|
||||
// truncated outside the gateway (or a stray `%` escape) is still a
|
||||
// string, and throwing here would break every API call — fall back
|
||||
// to the raw value, as `getLocaleFromCookie` does.
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@ -58,3 +58,32 @@ describe("api fetcher unauthorized redirect", () => {
|
||||
expect(window.location.href).not.toContain("next=");
|
||||
});
|
||||
});
|
||||
|
||||
describe("api fetcher CSRF cookies", () => {
|
||||
afterEach(() => {
|
||||
document.cookie = "theme=; max-age=0; path=/";
|
||||
document.cookie = "csrf_token=; max-age=0; path=/";
|
||||
rs.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("forwards a malformed CSRF cookie after an unrelated malformed cookie", async () => {
|
||||
const sentHeaders: Headers[] = [];
|
||||
rs.stubGlobal(
|
||||
"fetch",
|
||||
rs.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
sentHeaders.push(new Headers(init?.headers));
|
||||
return new Response("ok");
|
||||
}),
|
||||
);
|
||||
// Each assignment sets one cookie; semicolon-separated tails are attributes.
|
||||
document.cookie = "theme=%E0; path=/";
|
||||
document.cookie = "csrf_token=%gh; path=/";
|
||||
|
||||
expect(document.cookie).toBe("theme=%E0; csrf_token=%gh");
|
||||
|
||||
await apiFetch("http://localhost/api/threads", { method: "POST" });
|
||||
|
||||
expect(sentHeaders).toHaveLength(1);
|
||||
expect(sentHeaders[0]?.get("X-CSRF-Token")).toBe("%gh");
|
||||
});
|
||||
});
|
||||
|
||||
@ -141,4 +141,23 @@ describe("api fetcher Request inputs", () => {
|
||||
expect(sent[0]?.headers.get("X-CSRF-Token")).toBe("init-token");
|
||||
expect(await sent[0]?.text()).toBe("payload");
|
||||
});
|
||||
|
||||
it.each(["%E0%A4%A", "abc%", "a+b%zz"])(
|
||||
"forwards a malformed CSRF cookie verbatim instead of throwing: %s",
|
||||
async (cookieValue) => {
|
||||
document.cookie = `csrf_token=${cookieValue}`;
|
||||
|
||||
await apiFetch("http://localhost/api/threads", { method: "POST" });
|
||||
|
||||
expect(sent[0]?.headers.get("X-CSRF-Token")).toBe(cookieValue);
|
||||
},
|
||||
);
|
||||
|
||||
it("still decodes a percent-encoded CSRF cookie", async () => {
|
||||
document.cookie = "csrf_token=cookie%2Dtoken";
|
||||
|
||||
await apiFetch("http://localhost/api/threads", { method: "POST" });
|
||||
|
||||
expect(sent[0]?.headers.get("X-CSRF-Token")).toBe("cookie-token");
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user