mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
fix(cabinet): harden global encoders against lone UTF-16 surrogates
A lone (unpaired) UTF-16 surrogate — a truncated emoji in a backend name/remark
embedded in a subscription/connection URL — makes encodeURI/encodeURIComponent throw
a URIError on iOS WebKit (V8: 'URI malformed'; Safari/JSC: 'String contained an
illegal UTF-16 sequence'). qrcode.react calls encodeURI internally, so such a value
crashed the QR render and tripped the page-level ErrorBoundary ('Something went
wrong / String contained an illegal UTF-16 sequence / Try again').
Rather than wrap each (current and future) call site — and third-party libs we can't
edit — sanitise at the single chokepoint: patch the global encodeURI/encodeURIComponent
at bootstrap to replace lone surrogates with U+FFFD (same remedy as toWellFormed()).
Fail-safe, not fail-broken: verified byte-identical output for all well-formed strings
(URLs unaffected) and no-throw on lone surrogates for the real call patterns (qrcode.react
and btoa(unescape(encodeURIComponent(...)))). A fast path skips the rewrite when no
surrogate code units are present, so the hot path (every request URL) is untouched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
|||||||
isFullscreen,
|
isFullscreen,
|
||||||
} from '@telegram-apps/sdk-react';
|
} from '@telegram-apps/sdk-react';
|
||||||
import { clearStaleSessionIfNeeded } from './utils/token';
|
import { clearStaleSessionIfNeeded } from './utils/token';
|
||||||
|
import { installEncodingSurrogateGuard } from './utils/installEncodingSurrogateGuard';
|
||||||
import { useAuthStore } from './store/auth';
|
import { useAuthStore } from './store/auth';
|
||||||
import { AppWithNavigator } from './AppWithNavigator';
|
import { AppWithNavigator } from './AppWithNavigator';
|
||||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||||
@@ -29,6 +30,12 @@ import { getCachedFullscreenEnabled, isTelegramMobile } from './hooks/useTelegra
|
|||||||
import { applyTelegramLanguage } from './i18n';
|
import { applyTelegramLanguage } from './i18n';
|
||||||
import './styles/globals.css';
|
import './styles/globals.css';
|
||||||
|
|
||||||
|
// Harden the global encoders against lone UTF-16 surrogates (truncated emoji in
|
||||||
|
// backend names/remarks) BEFORE anything renders or fetches — otherwise such a
|
||||||
|
// string crashes any encodeURI/encodeURIComponent/btoa path on iOS WebKit,
|
||||||
|
// including qrcode.react's internal encodeURI. See installEncodingSurrogateGuard.
|
||||||
|
installEncodingSurrogateGuard();
|
||||||
|
|
||||||
// Polyfill Object.hasOwn for older iOS/Android WebViews (Safari < 15.4, old Chrome).
|
// Polyfill Object.hasOwn for older iOS/Android WebViews (Safari < 15.4, old Chrome).
|
||||||
// @telegram-apps/sdk v3 depends on valibot which uses Object.hasOwn internally.
|
// @telegram-apps/sdk v3 depends on valibot which uses Object.hasOwn internally.
|
||||||
// Without this, init() throws LaunchParamsRetrieveError on affected devices.
|
// Without this, init() throws LaunchParamsRetrieveError on affected devices.
|
||||||
|
|||||||
39
src/utils/installEncodingSurrogateGuard.ts
Normal file
39
src/utils/installEncodingSurrogateGuard.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { sanitizeSurrogates } from './sanitizeSurrogates';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App-wide guard against the "String contained an illegal UTF-16 sequence" crash.
|
||||||
|
*
|
||||||
|
* A lone (unpaired) UTF-16 surrogate — typically a truncated emoji in a backend
|
||||||
|
* name/remark that ends up in a subscription/connection URL — makes encodeURI /
|
||||||
|
* encodeURIComponent throw a URIError on iOS WebKit/JavaScriptCore (V8: "URI
|
||||||
|
* malformed"; Safari/JSC: "String contained an illegal UTF-16 sequence"). Any
|
||||||
|
* code path that encodes such a string crashes — including third-party libs we
|
||||||
|
* can't edit (e.g. qrcode.react calls encodeURI internally) and any encode call
|
||||||
|
* added later. The base64 idiom btoa(unescape(encodeURIComponent(x))) is covered
|
||||||
|
* transitively, since its encodeURIComponent is guarded here. (We do NOT patch
|
||||||
|
* btoa itself: it rejects every char > U+00FF — Cyrillic, emoji, even the U+FFFD
|
||||||
|
* replacement — with a separate "Invalid character" error that sanitising
|
||||||
|
* surrogates cannot fix, and nothing base64-encodes raw Unicode directly.)
|
||||||
|
*
|
||||||
|
* Instead of wrapping every (current and future) call site, we sanitise at the
|
||||||
|
* single chokepoint: the global encoders themselves. This is fail-safe, not
|
||||||
|
* fail-broken — for any well-formed string there are no lone surrogates, so the
|
||||||
|
* output is byte-for-byte identical; only strings that would otherwise have
|
||||||
|
* thrown get their lone surrogates replaced with U+FFFD (the same remedy as
|
||||||
|
* String.prototype.toWellFormed()).
|
||||||
|
*
|
||||||
|
* Must run before any rendering or network call. Idempotent.
|
||||||
|
*/
|
||||||
|
export function installEncodingSurrogateGuard(): void {
|
||||||
|
const flag = '__surrogateEncoderGuardInstalled';
|
||||||
|
const g = globalThis as typeof globalThis & Record<string, unknown>;
|
||||||
|
if (g[flag]) return;
|
||||||
|
g[flag] = true;
|
||||||
|
|
||||||
|
const nativeEncodeURI = globalThis.encodeURI;
|
||||||
|
const nativeEncodeURIComponent = globalThis.encodeURIComponent;
|
||||||
|
|
||||||
|
globalThis.encodeURI = (uri: string): string => nativeEncodeURI(sanitizeSurrogates(String(uri)));
|
||||||
|
globalThis.encodeURIComponent = (uriComponent: string | number | boolean): string =>
|
||||||
|
nativeEncodeURIComponent(sanitizeSurrogates(String(uriComponent)));
|
||||||
|
}
|
||||||
45
src/utils/sanitizeSurrogates.ts
Normal file
45
src/utils/sanitizeSurrogates.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Replace unpaired UTF-16 surrogates with the Unicode replacement char (U+FFFD).
|
||||||
|
*
|
||||||
|
* Why: a lone surrogate — e.g. a truncated emoji in a server/profile name embedded
|
||||||
|
* in a subscription URL's `#remark` — makes `encodeURI()` throw. On V8 the message is
|
||||||
|
* "URI malformed"; on iOS WebKit (JavaScriptCore, used by Telegram on iOS) it is
|
||||||
|
* "String contained an illegal UTF-16 sequence". `qrcode.react` calls `encodeURI`
|
||||||
|
* internally (qrcodegen `toUtf8ByteArray`), so such a string crashes the QR render and
|
||||||
|
* trips the page-level ErrorBoundary. `btoa(unescape(encodeURIComponent(...)))` and the
|
||||||
|
* Telegram native bridge throw on the same input.
|
||||||
|
*
|
||||||
|
* Sanitising keeps the link working (the surrogate only ever lives in a cosmetic remark)
|
||||||
|
* and renders a replacement glyph instead of taking the whole page down. Implemented
|
||||||
|
* without lookbehind for broad WebView compatibility.
|
||||||
|
*/
|
||||||
|
// Any surrogate code unit at all (paired or not). The vast majority of strings
|
||||||
|
// (ASCII URLs, ids, tokens) contain none, so this lets us bail out in O(1) before
|
||||||
|
// the per-char rebuild — important because the global encoder guard runs this on
|
||||||
|
// every encodeURIComponent/encodeURI/btoa call.
|
||||||
|
const SURROGATE_RANGE = /[\uD800-\uDFFF]/;
|
||||||
|
|
||||||
|
export function sanitizeSurrogates(value: string): string {
|
||||||
|
if (!SURROGATE_RANGE.test(value)) return value;
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
const code = value.charCodeAt(i);
|
||||||
|
if (code >= 0xd800 && code <= 0xdbff) {
|
||||||
|
// High surrogate: valid only if immediately followed by a low surrogate.
|
||||||
|
const next = value.charCodeAt(i + 1);
|
||||||
|
if (next >= 0xdc00 && next <= 0xdfff) {
|
||||||
|
result += value[i] + value[i + 1];
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
result += '<27>';
|
||||||
|
}
|
||||||
|
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
||||||
|
// Low surrogate with no preceding high surrogate.
|
||||||
|
result += '<27>';
|
||||||
|
} else {
|
||||||
|
result += value[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user