From a744b41910743e9604de669535f56a614fa269f1 Mon Sep 17 00:00:00 2001 From: Fringg Date: Wed, 11 Feb 2026 02:58:04 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20harden=20OAuth=20login=20flow=20?= =?UTF-8?q?=E2=80=94=20open=20redirect,=20path=20traversal,=20info=20leak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Validate authorize_url is HTTPS before redirect (open redirect prevention) - encodeURIComponent on provider name in API URL paths (path traversal prevention) - encodeURIComponent on referralCode in t.me deep link - Gate console.warn behind import.meta.env.DEV (no info leak in prod) --- src/api/auth.ts | 4 ++-- src/pages/Login.tsx | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/api/auth.ts b/src/api/auth.ts index a9b0552..4cf23a5 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -138,7 +138,7 @@ export const authApi = { provider: string, ): Promise<{ authorize_url: string; state: string }> => { const response = await apiClient.get<{ authorize_url: string; state: string }>( - `/cabinet/auth/oauth/${provider}/authorize`, + `/cabinet/auth/oauth/${encodeURIComponent(provider)}/authorize`, ); return response.data; }, @@ -146,7 +146,7 @@ export const authApi = { // OAuth: callback (exchange code for tokens) oauthCallback: async (provider: string, code: string, state: string): Promise => { const response = await apiClient.post( - `/cabinet/auth/oauth/${provider}/callback`, + `/cabinet/auth/oauth/${encodeURIComponent(provider)}/callback`, { code, state }, ); return response.data; diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 1cc840f..e3c71bf 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -109,6 +109,17 @@ export default function Login() { setOauthLoading(provider); try { const { authorize_url, state } = await authApi.getOAuthAuthorizeUrl(provider); + + // Validate redirect URL — only allow HTTPS to prevent open redirect + try { + const parsed = new URL(authorize_url); + if (parsed.protocol !== 'https:') { + throw new Error('Invalid OAuth redirect URL'); + } + } catch { + throw new Error('Invalid OAuth redirect URL'); + } + saveOAuthState(state, provider); window.location.href = authorize_url; } catch { @@ -122,7 +133,7 @@ export default function Login() { // If email auth is disabled but user came with ref param, redirect to bot useEffect(() => { if (referralCode && emailAuthConfig?.enabled === false && botUsername) { - window.location.href = `https://t.me/${botUsername}?start=${referralCode}`; + window.location.href = `https://t.me/${botUsername}?start=${encodeURIComponent(referralCode)}`; } }, [referralCode, emailAuthConfig, botUsername]); @@ -165,7 +176,8 @@ export default function Login() { const error = err as { response?: { status?: number; data?: { detail?: string } } }; const status = error.response?.status; const detail = error.response?.data?.detail; - console.warn(`Telegram auth attempt ${attempt + 1} failed:`, status, detail); + if (import.meta.env.DEV) + console.warn(`Telegram auth attempt ${attempt + 1} failed:`, status, detail); if (status === 401 && attempt < MAX_RETRIES) { await new Promise((r) => setTimeout(r, 1500)); @@ -200,7 +212,7 @@ export default function Login() { const error = err as { response?: { status?: number; data?: { detail?: string } } }; const status = error.response?.status; const detail = error.response?.data?.detail; - console.warn('Telegram auth retry failed:', status, detail); + if (import.meta.env.DEV) console.warn('Telegram auth retry failed:', status, detail); setError( detail || t('auth.telegramRetryFailed', 'Authorization failed. Close the app and try again.'),