mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
fix(security): extract & reuse getSafeRedirectPath, plug TopUpAmount returnTo
TelegramRedirect already had a local getSafeRedirectUrl helper that
collapsed protocol-relative URLs, absolute URLs, exotic schemes, and
URL-encoded forms down to '/'. TopUpAmount.handleSuccess was navigating
straight to a user-supplied returnTo query param without that filter —
not externally exploitable through react-router's navigate() (it doesn't
trigger an external nav), but a crafted link could produce ugly path
artefacts ('?returnTo=https://evil.com' would land the user at
/balance/top-up/<method>/https://evil.com).
Hoist the helper to src/utils/safeRedirect.ts, rename to
getSafeRedirectPath, reuse it in TelegramRedirect, and wrap TopUpAmount's
returnTo through it before navigate().
This commit is contained in:
@@ -7,26 +7,7 @@ import { useShallow } from 'zustand/shallow';
|
||||
import { brandingApi } from '../api/branding';
|
||||
import { isInTelegramWebApp, getTelegramInitData } from '../hooks/useTelegramSDK';
|
||||
import { tokenStorage } from '../utils/token';
|
||||
|
||||
// Validate redirect URL to prevent open redirect attacks
|
||||
const getSafeRedirectUrl = (url: string | null): string => {
|
||||
if (!url) return '/';
|
||||
// Only allow relative paths starting with /
|
||||
// Block protocol-relative URLs (//evil.com) and absolute URLs
|
||||
if (!url.startsWith('/') || url.startsWith('//')) {
|
||||
return '/';
|
||||
}
|
||||
// Additional check for encoded characters that could bypass validation
|
||||
try {
|
||||
const decoded = decodeURIComponent(url);
|
||||
if (!decoded.startsWith('/') || decoded.startsWith('//') || decoded.includes('://')) {
|
||||
return '/';
|
||||
}
|
||||
} catch {
|
||||
return '/';
|
||||
}
|
||||
return url;
|
||||
};
|
||||
import { getSafeRedirectPath } from '../utils/safeRedirect';
|
||||
|
||||
const MAX_RETRY_ATTEMPTS = 3;
|
||||
const RETRY_COUNT_KEY = 'telegram_redirect_retry_count';
|
||||
@@ -65,7 +46,7 @@ export default function TelegramRedirect() {
|
||||
const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null;
|
||||
|
||||
// Get redirect target from URL params (validated)
|
||||
const redirectTo = getSafeRedirectUrl(searchParams.get('redirect'));
|
||||
const redirectTo = getSafeRedirectPath(searchParams.get('redirect'));
|
||||
|
||||
useEffect(() => {
|
||||
// All timers scheduled inside this effect funnel through `timers` so the
|
||||
|
||||
@@ -13,6 +13,7 @@ import { staggerContainer, staggerItem } from '@/components/motion/transitions';
|
||||
import type { PaymentMethod, PaymentMethodOption } from '../types';
|
||||
import BentoCard from '../components/ui/BentoCard';
|
||||
import { saveTopUpPendingInfo } from '../utils/topUpStorage';
|
||||
import { getSafeRedirectPath } from '../utils/safeRedirect';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
|
||||
// Icons
|
||||
@@ -145,7 +146,12 @@ export default function TopUpAmount() {
|
||||
}, [navigate]);
|
||||
|
||||
const handleSuccess = useCallback(() => {
|
||||
navigate(returnTo || '/balance', { replace: true });
|
||||
// returnTo arrives via query string — validate as an in-app path before
|
||||
// navigate(), otherwise an absolute or encoded URL produces ugly
|
||||
// path artefacts in the URL bar. The validator returns '/' for invalid
|
||||
// input; treat that case as "no returnTo" and use the /balance default.
|
||||
const safe = getSafeRedirectPath(returnTo);
|
||||
navigate(returnTo && safe !== '/' ? safe : '/balance', { replace: true });
|
||||
}, [navigate, returnTo]);
|
||||
|
||||
// Keyboard: Escape to go back
|
||||
|
||||
Reference in New Issue
Block a user