From b4f066942a613027d8a9c347332ef1efdd29e03d Mon Sep 17 00:00:00 2001 From: c0mrade Date: Thu, 4 Jun 2026 16:54:50 +0300 Subject: [PATCH] fix(security): validate the deep-link url in miniapp/redirect.html (open redirect + DOM-XSS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit public/miniapp/redirect.html read the `url` query param and assigned it to both the manual and window.location.href with no validation. Served on the cabinet's own origin, this was an open redirect (phishing) and — in Telegram / WebView contexts that execute javascript: URIs in top-level navigation — a DOM-XSS able to exfiltrate the refresh_token from localStorage (account takeover). Add isSafeAppLink(): only a custom app deep link passes (scheme://, excluding http/https/file/blob/about/intent/content and the javascript/data/vbscript script schemes); anything else renders the no-URL state instead of navigating or setting href. Mirrors the validation already in src/pages/DeepLinkRedirect.tsx. --- public/miniapp/redirect.html | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/public/miniapp/redirect.html b/public/miniapp/redirect.html index 81de650..b6e5151 100644 --- a/public/miniapp/redirect.html +++ b/public/miniapp/redirect.html @@ -124,7 +124,21 @@ document.getElementById('errorText').textContent = t.error; document.getElementById('manualBtn').textContent = t.btn; - if (!url) { + // SECURITY: this page navigates to a user-supplied `url`. Only open + // a custom app deep link. A bare `javascript:`/`data:`/`vbscript:`/ + // `file:` URI (DOM-XSS) is not scheme://-shaped, and http(s)/intent + // are blocked (open redirect / intent abuse) — so only VPN app + // schemes (happ://, vless://, ...) pass. Mirrors the validation in + // src/pages/DeepLinkRedirect.tsx. + function isSafeAppLink(raw) { + var s = String(raw || '').trim().toLowerCase(); + var m = s.match(/^([a-z][a-z0-9+.\-]*):\/\//); + if (!m) return false; + var blocked = ['http', 'https', 'file', 'blob', 'about', 'javascript', 'data', 'vbscript', 'intent', 'content']; + return blocked.indexOf(m[1]) === -1; + } + + if (!url || !isSafeAppLink(url)) { document.getElementById('title').textContent = t.noUrl; document.getElementById('subtitle').textContent = ''; document.querySelector('.spinner').style.display = 'none';