mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
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:
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user