diff --git a/public/miniapp/redirect.html b/public/miniapp/redirect.html
index 93fe00a..3382186 100644
--- a/public/miniapp/redirect.html
+++ b/public/miniapp/redirect.html
@@ -129,19 +129,21 @@
// `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)) {
+ // src/pages/DeepLinkRedirect.tsx. Both checks are INLINE
+ // prefix-anchored RegExp tests so CodeQL recognizes them as taint
+ // barriers (js/xss, js/client-side-unvalidated-url-redirection).
+ function showNoUrl() {
document.getElementById('title').textContent = t.noUrl;
document.getElementById('subtitle').textContent = '';
document.querySelector('.spinner').style.display = 'none';
+ }
+
+ if (!url || !/^[a-z][a-z0-9+.\-]*:\/\//i.test(url)) {
+ showNoUrl();
+ return;
+ }
+ if (/^(https?|file|blob|about|javascript|data|vbscript|intent|content|filesystem):/i.test(url)) {
+ showNoUrl();
return;
}
diff --git a/src/utils/openAppScheme.ts b/src/utils/openAppScheme.ts
index 9c05d98..8fa3add 100644
--- a/src/utils/openAppScheme.ts
+++ b/src/utils/openAppScheme.ts
@@ -31,38 +31,15 @@ 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;
+ // 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:…'`. Both guards are INLINE
+ // prefix-anchored RegExp tests so CodeQL recognizes them as taint barriers
+ // (js/xss, js/client-side-unvalidated-url-redirection).
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) return;
+ if (/^(javascript|data|vbscript|file|blob|about|intent|content|filesystem):/i.test(url)) return;
const isHttp = /^https?:\/\//i.test(url);
if (isHttp) {