diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index e76889cf4d4..f6c6da6a620 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -1651,11 +1651,6 @@ "count": 1 } }, - "src/lib/telegram/initData.ts": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, "src/lib/usage/apiKeySelfService.ts": { "no-restricted-syntax": { "count": 1 diff --git a/src/lib/telegram/initData.ts b/src/lib/telegram/initData.ts index 479d9063da9..43c696546dd 100644 --- a/src/lib/telegram/initData.ts +++ b/src/lib/telegram/initData.ts @@ -12,7 +12,7 @@ * directly unit-testable. Never trust the client-side `initData` alone — * verification MUST happen server-side. */ -import { createHash, createHmac, timingSafeEqual } from "node:crypto"; +import { createHmac, timingSafeEqual } from "node:crypto"; /** Parse a URLSearchParams-style initData string into a record. */ export function parseInitData(initData: string): Record { @@ -21,8 +21,17 @@ export function parseInitData(initData: string): Record { for (const pair of initData.split("&")) { const eq = pair.indexOf("="); if (eq <= 0) continue; - const key = decodeURIComponent(pair.slice(0, eq)); - const value = decodeURIComponent(pair.slice(eq + 1)); + let key: string; + let value: string; + try { + key = decodeURIComponent(pair.slice(0, eq)); + value = decodeURIComponent(pair.slice(eq + 1)); + } catch { + // A malformed percent escape is not a valid initData field. Skip the pair + // so the caller gets a signature mismatch (401) instead of an uncaught + // URIError (500). + continue; + } if (key && !(key in out)) out[key] = value; } return out; diff --git a/tests/unit/telegram-init-data.test.ts b/tests/unit/telegram-init-data.test.ts index d755acba6a7..730399d07f6 100644 --- a/tests/unit/telegram-init-data.test.ts +++ b/tests/unit/telegram-init-data.test.ts @@ -71,3 +71,10 @@ test("verifyInitData handles chunked/encoded keys", () => { }); assert.equal(verifyInitData(initData, BOT_TOKEN), true); }); + +test("verifyInitData returns false on a malformed percent escape instead of throwing", () => { + assert.equal(verifyInitData("user=%ZZ&hash=abc", BOT_TOKEN), false); + const parsed = parseInitData("user=%ZZ&hash=abc"); + assert.equal(parsed.user, undefined); + assert.equal(parsed.hash, "abc"); +});