From aa6bf44db9808aa79e0d711c0331e75171be75c0 Mon Sep 17 00:00:00 2001 From: Fringg Date: Sat, 25 Jul 2026 00:20:52 +0300 Subject: [PATCH] =?UTF-8?q?fix(security):=20=D0=B2=D0=BD=D1=83=D1=82=D1=80?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D0=B8=D0=B9=20=D0=B3=D0=B0=D1=80=D0=B4=20ope?= =?UTF-8?q?nAppScheme=20=D0=BE=D1=82=20=D0=BE=D0=BF=D0=B0=D1=81=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D1=81=D1=85=D0=B5=D0=BC=20(CodeQL)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Callers валидируют вход (allowlist в DeepLinkRedirect, конфиг в Connection), но сама утилита доверяла аргументу — забытая валидация в будущем call-site стала бы client-side XSS через location.href='javascript:…'. Теперь схема парсится и javascript/data/ vbscript/file/blob/about/intent/content/filesystem отбрасываются до любого sink'а (зеркало isSafeAppLink из redirect.html). --- src/utils/openAppScheme.test.ts | 31 +++++++++++++++++++++++++++++++ src/utils/openAppScheme.ts | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/utils/openAppScheme.test.ts b/src/utils/openAppScheme.test.ts index d4f3cac..c9177a4 100644 --- a/src/utils/openAppScheme.test.ts +++ b/src/utils/openAppScheme.test.ts @@ -88,3 +88,34 @@ describe('openAppScheme', () => { expect(createdIframes).toHaveLength(1); }); }); + +describe('openAppScheme guard (defense-in-depth)', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it.each([ + 'javascript:alert(1)', + 'javascript://%0aalert(1)', + 'data:text/html,', + 'vbscript://x', + 'file:///etc/passwd', + 'intent://scan/#Intent;end', + 'no-scheme-at-all', + ])('never navigates for dangerous input (%s)', (url) => { + const { location, createdIframes } = setup({ + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Safari/605.1', + }); + openAppScheme(url); + expect(location.href).toBe(''); + expect(createdIframes).toHaveLength(0); + }); + + it('still opens valid app schemes after the guard', () => { + const { location } = setup({ + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Safari/605.1', + }); + openAppScheme('happ://add/https://sp.example.com/abc'); + expect(location.href).toBe('happ://add/https://sp.example.com/abc'); + }); +}); diff --git a/src/utils/openAppScheme.ts b/src/utils/openAppScheme.ts index 2876cb0..9c05d98 100644 --- a/src/utils/openAppScheme.ts +++ b/src/utils/openAppScheme.ts @@ -31,7 +31,39 @@ function isIOS(): boolean { return navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1; } +// Schemes that must never reach a navigation sink even if a caller forgot to +// validate: javascript:/data: are XSS vectors, the rest are abuse surfaces. +// Mirrors isSafeAppLink in public/miniapp/redirect.html. +const BLOCKED_SCHEMES = new Set([ + 'javascript', + 'data', + 'vbscript', + 'file', + 'blob', + 'about', + 'intent', + 'content', + 'filesystem', +]); + +/** + * Defense-in-depth: callers (DeepLinkRedirect allowlist, Connection config) + * validate upstream, but the utility itself must not trust its input — a + * missed validation at a future call site would otherwise become client-side + * XSS via `location.href = 'javascript:…'`. + */ +function isSafeToOpen(url: string): boolean { + const match = url + .trim() + .toLowerCase() + .match(/^([a-z][a-z0-9+.-]*):\/\//); + if (!match) return false; + return !BLOCKED_SCHEMES.has(match[1]); +} + export function openAppScheme(url: string): void { + if (!isSafeToOpen(url)) return; + const isHttp = /^https?:\/\//i.test(url); if (isHttp) { window.location.href = url;