fix(security): внутренний гард openAppScheme от опасных схем (CodeQL)

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).
This commit is contained in:
Fringg
2026-07-25 00:20:52 +03:00
parent 17e27d3437
commit aa6bf44db9
2 changed files with 63 additions and 0 deletions

View File

@@ -88,3 +88,34 @@ describe('openAppScheme', () => {
expect(createdIframes).toHaveLength(1); expect(createdIframes).toHaveLength(1);
}); });
}); });
describe('openAppScheme guard (defense-in-depth)', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it.each([
'javascript:alert(1)',
'javascript://%0aalert(1)',
'data:text/html,<script>alert(1)</script>',
'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');
});
});

View File

@@ -31,7 +31,39 @@ function isIOS(): boolean {
return navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1; 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 { export function openAppScheme(url: string): void {
if (!isSafeToOpen(url)) return;
const isHttp = /^https?:\/\//i.test(url); const isHttp = /^https?:\/\//i.test(url);
if (isHttp) { if (isHttp) {
window.location.href = url; window.location.href = url;