From 6a5018d7feb7775bf336384ac38be99d7e3f825e Mon Sep 17 00:00:00 2001 From: tuanaiseo Date: Sun, 12 Apr 2026 06:44:51 +0700 Subject: [PATCH 1/2] fix(security): oauth callback does not validate `state` parameter The OAuth callback flow accepts an authorization `code` from the URL and proceeds with token exchange, but there is no validation of the OAuth `state` parameter to bind the response to the original auth request. This can enable login CSRF/session mix-up attacks where an attacker injects a valid code for a different authorization context. Affected files: OAuthCallback.tsx, AppRenderer.tsx Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com> --- client/src/components/OAuthCallback.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/src/components/OAuthCallback.tsx b/client/src/components/OAuthCallback.tsx index ccfd6d928..282e31e0c 100644 --- a/client/src/components/OAuthCallback.tsx +++ b/client/src/components/OAuthCallback.tsx @@ -12,6 +12,8 @@ interface OAuthCallbackProps { onConnect: (serverUrl: string) => void; } +const OAUTH_STATE_SESSION_KEY = "oauth_state"; + const OAuthCallback = ({ onConnect }: OAuthCallbackProps) => { const { toast } = useToast(); const hasProcessedRef = useRef(false); @@ -36,6 +38,13 @@ const OAuthCallback = ({ onConnect }: OAuthCallbackProps) => { return notifyError(generateOAuthErrorDescription(params)); } + const callbackState = new URLSearchParams(window.location.search).get("state"); + const storedState = sessionStorage.getItem(OAUTH_STATE_SESSION_KEY); + if (!callbackState || !storedState || callbackState !== storedState) { + return notifyError("Invalid OAuth state"); + } + sessionStorage.removeItem(OAUTH_STATE_SESSION_KEY); + const serverUrl = sessionStorage.getItem(SESSION_KEYS.SERVER_URL); if (!serverUrl) { return notifyError("Missing Server URL"); From 4a10a2e26efa83dd5332ece77db5852f7b469eae Mon Sep 17 00:00:00 2001 From: tuanaiseo Date: Sun, 12 Apr 2026 06:44:52 +0700 Subject: [PATCH 2/2] fix(security): oauth callback does not validate `state` parameter The OAuth callback flow accepts an authorization `code` from the URL and proceeds with token exchange, but there is no validation of the OAuth `state` parameter to bind the response to the original auth request. This can enable login CSRF/session mix-up attacks where an attacker injects a valid code for a different authorization context. Affected files: OAuthCallback.tsx, AppRenderer.tsx Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com> --- client/src/components/AppRenderer.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/client/src/components/AppRenderer.tsx b/client/src/components/AppRenderer.tsx index e25f35c91..d8c44416b 100644 --- a/client/src/components/AppRenderer.tsx +++ b/client/src/components/AppRenderer.tsx @@ -31,6 +31,8 @@ interface AppRendererProps { onNotification?: (notification: ServerNotification) => void; } +const OAUTH_STATE_SESSION_KEY = "oauth_state"; + const AppRenderer = ({ sandboxPath, tool, @@ -74,8 +76,22 @@ const AppRenderer = ({ const handleOpenLink = async ({ url }: { url: string }) => { let isError = true; if (url.startsWith("https://") || url.startsWith("http://")) { - window.open(url, "_blank"); - isError = false; + try { + const nextUrl = new URL(url); + if (nextUrl.searchParams.get("response_type") === "code") { + const stateBytes = new Uint8Array(16); + window.crypto.getRandomValues(stateBytes); + const state = Array.from(stateBytes, (byte) => + byte.toString(16).padStart(2, "0"), + ).join(""); + sessionStorage.setItem(OAUTH_STATE_SESSION_KEY, state); + nextUrl.searchParams.set("state", state); + } + window.open(nextUrl.toString(), "_blank"); + isError = false; + } catch { + isError = true; + } } return { isError }; };