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:
c0mrade
2026-05-26 16:13:33 +03:00
parent b6613ae4c9
commit 424a19344d
3 changed files with 40 additions and 22 deletions

31
src/utils/safeRedirect.ts Normal file
View File

@@ -0,0 +1,31 @@
/**
* Normalise a user-supplied redirect / returnTo URL down to a safe in-app path.
*
* Returns the input unchanged when it is a plain absolute path (`/foo/bar`).
* Returns `/` otherwise — for protocol-relative URLs (`//evil.com`), absolute
* URLs (`https://…`), exotic schemes (`javascript:`, `data:`), or anything
* that smuggles a host through URL encoding (`%2F%2Fevil.com`).
*
* Even with react-router's navigate() — which only treats inputs as paths
* and won't trigger an external nav — pasted absolute URLs would still
* produce ugly path artifacts. Centralising the check matches what
* TelegramRedirect already did and gives every returnTo entry the same
* shape.
*/
export function getSafeRedirectPath(url: string | null | undefined): string {
if (!url) return '/';
// Only allow relative paths starting with /
if (!url.startsWith('/') || url.startsWith('//')) {
return '/';
}
// Catch the encoded forms (//evil.com → %2F%2Fevil.com, scheme://…)
try {
const decoded = decodeURIComponent(url);
if (!decoded.startsWith('/') || decoded.startsWith('//') || decoded.includes('://')) {
return '/';
}
} catch {
return '/';
}
return url;
}